11

I have a bunch of C# classes, which are auto generated from an XSD. Then I generate XML files based on those C# classes. Nothing existing so far.

The problem:

The generated XML files are going through validation and the validation requires an extra attribute to all XML tags with xsi:nil="true". Basically the tags should look like : <testTag.01 xsi:nil="true" NV="123123" />, but I can't achieve that in C#. My code is:

     if (myObject.TestTag.HasValue)
        {
            t.testTag01 = new testTag01();
            t.testTag01.Value = myObject.TestTag.Value;
        }
        //else
        //{
        //    t.testTag01 = new testTag01();
        //    t.testTag01.NV = "123123";//Not Recorded
        //}

This code generates <testTag.01>SomeValue</testTag.01> or <testTag.01 xsi:nil="true"/>.

If I uncomment the ELSE, the result would be: <testTag.01>SomeValue</testTag.01> or <testTag.01 NV="123123" />.

So I have no idea how to get to the format, which is required by the validation tool. Any ideas ?

P.S.

Here is the auto-generated C# class:

/// [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://www.blabla.org")]

public partial class testTag01 {

private string nvField;

private SomeEnum valueField;

/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string NV {
    get {
        return this.nvField;
    }
    set {
        this.nvField = value;
    }
}

/// <remarks/>
[System.Xml.Serialization.XmlTextAttribute()]
public SomeEnum Value {
    get {
        return this.valueField;
    }
    set {
        this.valueField = value;
    }
} }

I wouldn't like to alter that part, but I understand it is impossible without doing it. Also I have tried to set SomeEnum to be Nullable. public SomeEnum? Value, but is throwing an exception:

Cannot serialize member 'Value' of type System.Nullable`1[]. XmlAttribute/XmlText cannot be used to encode complex types.
Tech0
  • 253
  • 3
  • 16
  • Need to see auto-generated classes. These usually needed to be edited. You shouldn't need to add code to produce nulls. – jdweng Oct 02 '15 at 13:23
  • I have put the class. Also I have tried to set the Value to Nullable, but it is throwing an exception. :-( – Tech0 Oct 06 '15 at 08:32
  • 1
    This will not work out of the box. See [**Xsi:nil Attribute Binding Support**: The nil attribute and other attributes](https://msdn.microsoft.com/en-us/library/ybce7f69.aspx). – dbc Oct 06 '15 at 11:09
  • Not sure I entirely understand your data model. Does your `testTag01` class ever have a `SomeValue` value, or is that there as an artificial requirement? – dbc Oct 06 '15 at 11:20

2 Answers2

1

XmlSerializer doesn't directly support binding to elements that simultaneously have xsi:nil="true" along with other attribute values; see Xsi:nil Attribute Binding Support: The nil attribute and other attributes.

Thus, you need to emit the attribute manually.

If you want to be able to generate an element with no content and two attributes, one named NV and the other always being xsi:nil="true", you can modify your testTag01 class to have the NV property as well as a synthetic property having the correct namespace and name:

public class testTag01 
{
    [XmlAttribute]
    public string NV { get; set; }

    [XmlAttribute("nil", Namespace = "http://www.w3.org/2001/XMLSchema-instance")]
    public string Nil { get { return "true"; } set { } }
}

If you sometimes want to have xsi:nil="true" but at other times want the element to have content corresponding to your SomeEnum, you need to do something a bit more complicated, since the xsi:nil="true" must be suppressed when the element has content:

public class testTag01
{
    [XmlAttribute]
    public string NV { get; set; }

    [XmlAttribute("nil", Namespace = "http://www.w3.org/2001/XMLSchema-instance")]
    public string Nil { get { return SomeEnum == null ? "true" : null; } set { } }

    public bool ShouldSerializeNil() { return SomeEnum == null; }

    [XmlIgnore]
    public SomeEnum? SomeEnum { get; set; }

    [XmlText]
    public string SomeEnumText
    {
        get
        {
            if (SomeEnum == null)
                return null;
            return SomeEnum.Value.ToString();
        }
        set
        {
            // See here if one needs to parse XmlEnumAttribute attributes
            // http://stackoverflow.com/questions/3047125/retrieve-enum-value-based-on-xmlenumattribute-name-value
            value = value.Trim();
            if (string.IsNullOrEmpty(value))
                SomeEnum = null;
            else
            {
                try
                {
                    SomeEnum = (SomeEnum)Enum.Parse(typeof(SomeEnum), value, false);
                }
                catch (Exception)
                {
                    SomeEnum = (SomeEnum)Enum.Parse(typeof(SomeEnum), value, true);
                }
            }
        }
    }
}

(An element that simultaneously has both xsi:nil="true" and content would be a violation of the XML standard; hopefully you don't have that.)

Then use it like:

public class TestClass
{
    [XmlElement("testTag.01")]
    public testTag01 TestTag { get; set; }

    public static void Test()
    {
        Test(new TestClass { TestTag = new testTag01 { NV = "123123" } });
        Test(new TestClass { TestTag = new testTag01 { NV = "123123", SomeEnum = SomeEnum.SomeValue } });
    }

    private static void Test(TestClass test)
    {
        var xml = test.GetXml();

        var test2 = xml.LoadFromXML<TestClass>();

        Console.WriteLine(test2.GetXml());
        Debug.WriteLine(test2.GetXml());

        if (test2.TestTag.NV != test.TestTag.NV)
        {
            throw new InvalidOperationException("test2.TestTag.NV != test.TestTag.NV");
        }
    }
}

The XML output looks like:

<TestClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <testTag.01 NV="123123" xsi:nil="true" />
</TestClass>

Or

<TestClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <testTag.01 NV="123123">SomeValue</testTag.01>
</TestClass>

Prototype fiddle using these extension methods:

public static class XmlSerializationHelper
{
    public static T LoadFromXML<T>(this string xmlString, XmlSerializer serializer = null)
    {
        T returnValue = default(T);

        using (StringReader reader = new StringReader(xmlString))
        {
            object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
            if (result is T)
            {
                returnValue = (T)result;
            }
        }
        return returnValue;
    }

    public static string GetXml<T>(this T obj, XmlSerializerNamespaces ns = null, XmlWriterSettings settings = null, XmlSerializer serializer = null)
    {
        using (var textWriter = new StringWriter())
        {
            settings = settings ?? new XmlWriterSettings() { Indent = true, IndentChars = "  " }; // For cosmetic purposes.
            using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                (serializer ?? new XmlSerializer(typeof(T))).Serialize(xmlWriter, obj, ns);
            return textWriter.ToString();
        }
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
0

As expected there is no solution for that case out of the box, so I improvise a bit and achieved my goal in a post processing logic.

I am parsing the generated XML and if I am looking for a node with xsi:nil attribute, but without NV attribute - I add NV attribute with default value. Same for the nodes with NV attribute, but no xsi:nil.

Here is the code:

        XmlDocument doc = new XmlDocument();// instantiate XmlDocument and load XML from file
        doc.Load("somepath.xml");

        //Get the nodes with NV attribute(using XPath) and add xsi:nill to that nodes
        XmlNodeList nodes = doc.SelectNodes("//*[@NV]");

        foreach (XmlNode node in nodes)
        {
            XmlAttribute nilAttr = doc.CreateAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance");
            nilAttr.Value = "true";
            node.Attributes.Append(nilAttr);
        }

        //Get the nodes with xsi:nill attribute(using XPath) and add NV with default value to that nodes
        XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
        nsManager.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
        XmlNodeList nilNodes = doc.SelectNodes("//*[@xsi:nil]", nsManager);

        foreach (XmlNode node in nilNodes)
        {
            XmlAttribute nvAttr = doc.CreateAttribute("NV");
            nvAttr.Value = "7701003";
            node.Attributes.Append(nvAttr);
        }

        doc.Save("somepath.xml");

The upper answer makes totally sense, but since these classes are auto-generated I will do it my way with the post processing, cause if the provider changes the XSD schema, my solution doesn't need any extra work. Thanks anyway.

Tech0
  • 253
  • 3
  • 16
  • 2
    Actually. depending on framework versions, etc, you should be able to emit the generated class as a partial. Then you can include the "always nil attribute" from above into a second class file instead. – Roger Willcocks Oct 13 '15 at 08:35