XML C# 中带有属性的可空元素的序列化

XML serialization of nillable elements with attributes in C#

我们使用属性 nilReason 来表达 XML 元素为空的原因。示例:

<dateOfDeath nilReason="noValue" xsi:nil="true"/>
<dateOfDeath nilReason="valueUnknown" xsi:nil="true"/>

在第一个例子中,这个人还活着,因为没有死亡日期。在第二个例子中,我们不知道死亡日期是什么。

该元素的XSD-定义如下:

<xs:element name="dateOfDeath" type="DateOfDeath" nillable="true"/>
<xs:complexType name="DateOfDeath">
    <xs:simpleContent>
        <xs:extension base="xs:date">
            <xs:attribute name="nilReason" type="NilReason"/>
        </xs:extension>
    </xs:simpleContent>
</xs:complexType>
<xs:simpleType name="NilReason">
    <xs:restriction base="xs:string">
        <xs:enumeration value="noValue"/>
        <xs:enumeration value="valueUnknown"/>
    </xs:restriction>
</xs:simpleType>

我 运行 使用 .net 框架提供的 XSD.exe 工具生成 C# 类 时遇到问题。如何编写生成以下内容的代码 XML?

<dateOfDeath nilReason="noValue" xsi:nil="true"/>

这是我能写出的最好的近似代码:

DateOfDeath dateOfDeath = new DateOfDeath();
dateOfDeath.nilReason = NilReason.noValue;
dateOfDeath.nilReasonSpecified = true;
XmlSerializer serializer = new XmlSerializer(typeof(DateOfDeath));
StreamWriter writer = new StreamWriter("dateofdeath.xml");
serializer.Serialize(writer, dateOfDeath);
writer.Close();

然而,不幸的是,这段代码产生了以下结果:

<dateOfDeath nilReason="noValue">0001-01-01</dateOfDeath>

这不是我想要的,因为它会生成一个虚拟日期值。看来这是序列化器的一个缺点。避免此问题的唯一方法似乎是应用一个函数,该函数删除虚拟值并在序列化后插入 xsi:nil="true" 属性。然后还需要一个在反序列化之前删除 xsi:nil="true" 属性的函数。否则nilReason-attribute的信息会在反序列化过程中被丢弃

问题是属性是在同一个 DateOfDeath 中与其值并排生成的 class(为简洁起见,我省略了一些代码):

public partial class DateOfDeath
{
    private NilReason nilReasonField;
    private bool nilReasonFieldSpecified;
    private System.DateTime valueField;

    [System.Xml.Serialization.XmlAttributeAttribute()]
    public NilReason nilReason
    {
        get/set...
    }

    [System.Xml.Serialization.XmlIgnoreAttribute()]
    public bool nilReasonSpecified
    {
        get/set...
    }

    [System.Xml.Serialization.XmlText(DataType = "date")]
    public System.DateTime Value
    {
        get/set...
    }
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.81.0")]
[System.SerializableAttribute()]
public enum NilReason
{
    noValue,
    valueUnknown,
}

因此,为了序列化一个 nil 元素,您必须将父元素设置为 null:

DateOfDeath dod = null;
serializer.Serialize(stream, dod);

生成如下内容:

<dateOfDeath xmlns:xsi="..." xmlns:xsd="..." xsi:nil="true" />

这当然会导致您无法设置属性:

DateOfDeath dod = null;
dod.nilReason = noValue; // does not work with nullpointer

然而,该值呈现为 xml 元素的文本,例如:

<dateOfDeath xmlns:xsi="..." xmlns:xsd="...">[value]</dateOfDeath>

其中 [value] 当然是您日期的文本表示。因此,即使您可以将值设置为 null - 因为您无法将复杂类型(例如 Nullable)呈现为 XmlText,所以您不能将其设置为 null - 您仍然无法将父 () 元素设置为 nil无论如何。

所以也许最接近您想要的是使值可为空并将其呈现为 XmlElement(注意添加的问号):

private System.DateTime? valueField;

[System.Xml.Serialization.XmlElement(DataType = "date", IsNullable = true)]
public System.DateTime? Value { get/set ...}

将其设置为空

DateOfDeath dod = new DateOfDeath();
dod.nilReason = NilReason.noValue;
dod.nilReasonSpecified = true;
dod.Value = null;

XmlSerializer serializer = new XmlSerializer(typeof(DateOfDeath));
serializer.Serialize(stream, dod);

给你:

<?xml version="1.0" encoding="utf-8"?>
<dateOfDeath xmlns:xsi="..." xmlns:xsd="..." nilReason="noValue">
  <Value xsi:nil="true" />
</dateOfDeath>

这显然不是您想要的,但是除非有一种神奇的方法可以将外部 class 成员作为属性附加到空指针或相反,否则请使用 [=] 的另一个成员38=] 作为零值指标,使用给定的工具链不可能实现这一目标。

接下来的两个函数解决了这个问题。第一个 (addNilAttributes) 将属性 xsi:nil="true" 添加到包含属性 nilReason 的元素,并使元素为空。此函数必须在序列化后应用。

    static public XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

    static public XElement addNilAttributes(XElement root)
    {       
        IEnumerable<XElement> noValueElements =
            from el in root.Descendants()
            where (string)el.Attribute("nilReason") != null
            select el;

        foreach (XElement el in noValueElements)
        {
            el.Add(new XAttribute(xsi + "nil", "true"));
            el.ReplaceNodes(null); // make element empty
        }

        IEnumerable<XElement> nilElements =
            from el in root.Descendants()
            where (string)el.Attribute("nilReason") == null && (string)el.Attribute(xsi + "nil") != null
            select el;

        nilElements.Remove();
        return root;
    }

例如,<dateOfDeath nilReason="noValue">0001-01-01</dateOfDeath>将被翻译成<dateOfDeath nilReason="noValue" xsi:nil="true"/>。但是 <dateOfDeath xsi:nil="true"/> 将被删除,因为您总是必须指定 nilReason 以防元素为空。

第二个函数 (removeNilAttributes) 在反序列化之前删除 xsi:nil 属性。否则nilReason属性的值会在反序列化过程中丢失。

    static public XElement removeNilAttributes(XElement root)
    {
        root.DescendantsAndSelf().Attributes(xsi + "nil").Remove();
        return root;
    }

比如<dateOfDeath nilReason="noValue" xsi:nil="true"/>在反序列化之前会被转换成<dateOfDeath nilReason="noValue"/>

下面是如何应用这两个函数的示例代码:

        DateOfDeath dateOfDeath = new DateOfDeath();
        dateOfDeath.nilReason = NilReasonType.noValue;
        dateOfDeath.nilReasonSpecified = true;

        XmlSerializer serializer = new XmlSerializer(typeof(DateOfDeath));

        StringWriter writer = new StringWriter();   
        serializer.Serialize(writer, dateOfDeath);
        String str = writer.ToString();
        Console.WriteLine(str);          
        writer.Close();

        XElement root = XElement.Parse(str);

        root = addNilAttributes(root);
        Console.WriteLine(root.ToString());

        root = removeNilAttributes(root);
        Console.WriteLine(root.ToString());

        StringReader reader = new StringReader(root.ToString());        
        DateOfDeath dateOfDeath2 = new DateOfDeath();
        dateOfDeath2 = (DateOfDeath)serializer.Deserialize(reader);

输出:

<dateOfDeath xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd=" tp://www.w3.org/2001/XMLSchema" nilReason="noValue">0001-01-01</dateOfDeath>

<dateOfDeath xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd=" tp://www.w3.org/2001/XMLSchema" nilReason="noValue" xsi:nil="true"/>

<dateOfDeath xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd=" tp://www.w3.org/2001/XMLSchema" nilReason="noValue"/>