使用多种类型反序列化XML

7

我正在尝试反序列化XML,其中一些相同名称的标签具有不同的xsi类型:

<user-defined-data-row>
  <field name="entity">
    <field-value xsi:type="field-text-valueType">
      <value>Test</value>
    </field-value>
  </field>
  <field name="expiry_date">
    <field-value xsi:type="field-date-valueType">
      <value>2001-10-07</value>
    </field-value>
  </field>
</user-defined-data-row>

将xml反序列化为以下模型即可轻松实现:

[XmlRoot(ElementName = "field-value", Namespace = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0")]
[XmlType("field-text-valueType")]
public class Fieldvalue
{
    [XmlElement(ElementName = "value", Namespace = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0")]
    public string Value { get; set; }
}

唯一不同的是XML中的类型:
field-text-valueType field-date-valueType 我该如何使C#类使用以下方式解释两种类型:
[XmlType("field-text-valueType")]

编辑:反序列化而非序列化


你是指反序列化吗? - stuartd
type属性指示类继承一个基类,其中field-value是基类名称,field-text-valueType是子类名称。 - jdweng
1个回答

7
您在XML中看到的xsi:type属性是标准的W3C XML Schema属性,允许元素明确指定其类型;详情请参见此处。正如Xsi:type Attribute Binding Support中所解释的那样,XmlSerializer支持通过XmlIncludeAttribute的使用来反序列化多态类型的机制。
首先,按照以下方式定义一个抽象基类FieldValue
public static class XmlNamespaces
{
    public const string Crsoftwareinc = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0";
}

[XmlRoot("field-value", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field-value", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlInclude(typeof(TextFieldValue)), 
XmlInclude(typeof(DateFieldValue))]
public abstract partial class FieldValue
{
    // It's not necessary to have this in the base class but I usually find it convenient.
    public abstract object GetValue();
}

接下来,对于每一个可能的 xsi:type="XXX" 的值,定义一个由 FieldValue 派生而来的类型,其 XmlTypeAttribute.TypeNamexsi:type 的值相匹配。对于每个已经显示的派生类型,在基类上使用 [XmlInclude(typeof(TDerivedFieldValue))] 属性进行装饰:

[XmlRoot("field-text-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field-text-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
public class TextFieldValue : FieldValue
{
    [XmlElement("value")]
    public string Value { get; set; }

    public override object GetValue() { return Value; }
}

[XmlRoot("field-date-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field-date-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
public class DateFieldValue : FieldValue
{
    [XmlElement("value", DataType = "date")]
    public DateTime Value { get; set; }

    public override object GetValue() { return Value; }
}

接下来,按照以下方式定义与<field>和其他更高级元素对应的包含类型:

[XmlRoot("field", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field", Namespace = XmlNamespaces.Crsoftwareinc)]
public class Field
{
    [XmlAttribute("name")]
    public string Name { get; set; }

    [XmlElement("field-value")]
    public FieldValue FieldValue { get; set; }
}

[XmlRoot("user-defined-data-row", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("user-defined-data-row", Namespace = XmlNamespaces.Crsoftwareinc)]
public class UserDefinedDataRow
{
    [XmlElement("field")]
    public List<Field> Fields { get; set; }
}

// The XML for the root object is not shown so this is just a stub
[XmlRoot("root", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("root", Namespace = XmlNamespaces.Crsoftwareinc)]
public class RootObject
{
    [XmlElement("user-defined-data-row")]
    public List<UserDefinedDataRow> Rows { get; set; }
}

Notes:
注意:
  • If the base class FieldValue has a namespace specified in via XmlTypeAttribute.Namespace, then the derived classes must also, or else an error will get thrown by XmlSerializer.

    Once an [XmlType] namespace is defined, it automatically applies to all serialized properties, so it isn't necessary to specify the same namespace via [XmlElement(Namespace = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0")] attributes.

  • I got tired of repeatedly typing the namespace "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0" and so I extracted it into a constant.

  • Other derived types of FieldType can be added easily, e.g.:

    [XmlRoot("field-decimal-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
    [XmlType("field-decimal-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
    public class DecimalFieldValue : FieldValue
    {
        [XmlElement("value")]
        public decimal Value { get; set; }
    
        public override object GetValue() { return Value; }
    }
    
    [XmlInclude(typeof(DecimalFieldValue))]
    public abstract partial class FieldValue { }
    

    Don't forget to add [XmlInclude(typeof(DecimalFieldValue))] when doing so.

  • If you have been given an XSD for the XML you are trying to deserialize that specifies the possible types of <field-value> via e.g. an <xsd:extension> element as shown in Generating XML Documents from XML Schemas: Abstract Types, then xsd.exe will generate classes that include an appropriate type hierarchy. But if you only have the XML, then xsd.exe and Paste XML as Classes will not generate a correct type hierarchy using whatever xsi:type attributes are present.

    For more about this limitation see xsi:type attribute messing up C# XML deserialization.

  • Your XML is not well-formed because it omits a declaration for the xsi: namespace. Also, a default namespace xmlns="http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0" is not defined so none of the elements are actually in this namespace. Thus I assume your XML is a fragment of some larger document that is valid, e.g.

    <root 
     xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xmlns="http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0">
      <user-defined-data-row>
          <!-- Remainder as shown in the question -->
      </user-defined-data-row>
    </root>
    

这里有一个示例工作的.Net fiddle,点击这里查看。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接