Henri Korver
Henri Korver

Reputation: 21

XML serialization of nillable elements with attributes in C#

We use the attribute nilReason to express the reason why an XML-element is empty. Examples:

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

In the first example, the person is still alive because there is no date of death. In the second example, we do not know what the date of death is.

The XSD-definition of this element is given below:

<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>

I run into problems when I generate C# classes with the XSD.exe tool that is provided by the .net framework. How do I write code that produces the following XML?

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

This is the best approximation code that I was able to write:

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

However, sadly, this code produces the following result:

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

which is not exactly what I want because it generates a dummy date value. It seems that this is a shortcoming of the serializer. The only way to circumvent this problem seems to be applying a function that removes the dummy value and inserts the xsi:nil="true" attribute after serialization. Then one also needs a function that removes the xsi:nil="true" attribute before deserialization. Otherwise the information of the nilReason-attribute will be thrown away during the deserialization process.

Upvotes: 2

Views: 2310

Answers (2)

Henri Korver
Henri Korver

Reputation: 21

The next two functions solve the problem. The first one (addNilAttributes) adds the attribute xsi:nil="true" to elements containing the attribute nilReason and makes the element empty. This function has to be applied after serialization.

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

For instance, <dateOfDeath nilReason="noValue">0001-01-01</dateOfDeath> will be translated into <dateOfDeath nilReason="noValue" xsi:nil="true"/>. But <dateOfDeath xsi:nil="true"/> will be removed because you always have to specify the nilReason in case the element is empty.

The second function (removeNilAttributes) removes the xsi:nil attributes before deserialization. Otherwise the value of the nilReason attribute will be lost during the deserialization process.

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

For instance, <dateOfDeath nilReason="noValue" xsi:nil="true"/> will be converted into <dateOfDeath nilReason="noValue"/> before deserialization.

Below some sample code how these two functions can be applied:

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

Output:

<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"/>

Upvotes: 0

Holger B&#246;hnke
Holger B&#246;hnke

Reputation: 1130

The problem is that the attribute is generated side by side with it's value, in the same DateOfDeath class (I left out some code for brevity):

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,
}

So in order to serialize a nil element you have to set the parent to null:

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

producing something like:

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

which of course renders you unable to set the attribute:

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

The value however is rendered as the xml element's text like:

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

Where [value] is of course the text representation of your date. So even if you could set value to null - which you cannot because you can't render a complex type (e.g. Nullable<DateTime>) as XmlText - you still would not be able to set the parent (<DateOfDeath>) element to nil anyway.

So maybe the closest to what you want is to make the value nullable and render it as XmlElement (notice the added questionmark):

private System.DateTime? valueField;

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

set that to null

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

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

giving you:

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

This is obviously not exacly what you wanted, but unless there's a magic way to either attach an outer class member as attribute to a null pointer or the other way round, use another member of your class as nil value indicator, there's no chance achieving this with the given toolchain.

Upvotes: 2

Related Questions