Search code examples
c#xmlxsdxmlserializersgen

How to use SGen to generate a serialization assembly for a XML schema that is mapped to a two-level array?


I'm consuming a third-party service that requires XmlSerializerFormat contracts; I want to speed up startup by creating a serialization assembly, but Sgen.exe really doesn't like a particular construct in the schema that Xsd.exe spits out a nested array for.

The schema includes sequences of elements nested two levels deep like so:

Foo.xsd

<xs:schema targetNamespace="http://example.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://example.com" elementFormDefault="qualified">
    <xs:element name="Foo" type="Foo"/>
    <xs:complexType name="Foo">
        <xs:sequence>
            <xs:element name="List" type="FooList" minOccurs="0" maxOccurs="unbounded"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="FooList">
        <xs:sequence>
            <xs:element name="Item" type="FooListItem" minOccurs="0" maxOccurs="unbounded"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="FooListItem">
        <xs:simpleContent>
            <xs:extension base="xs:string"/>
        </xs:simpleContent>
    </xs:complexType>
</xs:schema>

That is: a toplevel Foo contains many FooLists, and a FooList contains many FooListItem.

Running xsd /c Foo.xsd produces the following:

Foo.cs

using System.Xml.Serialization;

[XmlType(Namespace="http://example.com")]
[XmlRoot(Namespace="http://example.com", IsNullable=false)]
public partial class Foo {

    private FooListItem[][] listField;

    [XmlArrayItem("Item", typeof(FooListItem), IsNullable=false)]
    public FooListItem[][] List {
        get {
            return this.listField;
        }
        set {
            this.listField = value;
        }
    }
}

[XmlType(Namespace="http://example.com")]
public partial class FooListItem {

    private string valueField;

    [XmlText]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

That is, no class for FooList is present for some reason, instead there's just a nested array of FooListItems.

However, when I build this and run Sgen.exe on the resulting DLL using just sgen /keep obj\Debug\net461\Foo.dll, this chokes on the following error messages:

error CS0030: Cannot convert type 'FooListItem[]' to 'FooListItem'
error CS0029: Cannot implicitly convert type 'FooListItem' to 'FooListItem[]'

(I'm using .NET 4.7 versions of Xsd.exe and Sgen.exe, I'm just targeting 4.6.1 for compatibility.)

Looking at the generated code, it chokes in the following method:

void Write3_Foo(string n, string ns, global::Foo o, bool isNullable, bool needType) {
    if ((object)o == null) {
        if (isNullable) WriteNullTagLiteral(n, ns);
        return;
    }
    if (!needType) {
        System.Type t = o.GetType();
        if (t == typeof(global::Foo)) {
        }
        else {
            throw CreateUnknownTypeException(o);
        }
    }
    WriteStartElement(n, ns, o, false, null);
    if (needType) WriteXsiType(@"Foo", @"http://example.com");
    {
        // THIS SEEMS TO BE THE ROOT CAUSE
        global::FooListItem[][] a = (global::FooListItem[][])((global::FooListItem[][])o.@List);
        if (a != null){
            WriteStartElement(@"List", @"http://example.com", null, false);
            for (int ia = 0; ia < a.Length; ia++) {
                // ERROR IS REPORTED HERE
                Write2_FooListItem(@"Item", @"http://example.com", ((global::FooListItem)a[ia]), false, false);
            }
            WriteEndElement();
        }
    }
    WriteEndElement(o);
}

So it seems like Xsd.exe and Sgen.exe try to realize the pattern where an element has an explicit "list of X" child containing X items without creating a separate class for the list element, but only relying on the name of the serialized property to synthetise the intermediate element; and this breaks when the list element itself may be repeated.

Is there a way to work around this? Like force Xsd.exe to generate a class for the intermediate element? I suppose this might be an actual bug in Xsd.exe and Sgen.exe, but that won't really help me in a reasonable time frame.


As said above, this is a third-party service; I have absolutely no control over the schema and the less manual editing of generated code involved the better since my actual files are tens of thousands of lines long.


Solution

  • The error is on this line

    From : [XmlArrayItem("Item", typeof(FooListItem), IsNullable=false)]

    To :"[XmlArrayItem("Item", IsNullable = false)]

    Here is sample of working code :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml;
    using System.Xml.Serialization;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            const string FILENAME = @"c:\temp\test.xml";
            static void Main(string[] args)
            {
    
                XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
                namespaces.Add("xs", "http://www.w3.org/2001/XMLSchema");
                namespaces.Add("", "http://example.com");
    
                XmlWriterSettings settings = new XmlWriterSettings();
                settings.Indent = true;
    
                XmlWriter writer = XmlWriter.Create(FILENAME, settings);
                XmlSerializer serializer = new XmlSerializer(typeof(Foo));
    
                Foo foo = new Foo()
                {
                    List = new FooListItem[][] {
                        new FooListItem[] { 
                            new FooListItem() { Value = "abc"},
                            new FooListItem() { Value = "abd"},
                            new FooListItem() { Value = "abe"}
                        },
                        new FooListItem[] { 
                            new FooListItem() { Value = "bbc"},
                            new FooListItem() { Value = "bbd"},
                            new FooListItem() { Value = "bbe"}
                        },
                        new FooListItem[] { 
                            new FooListItem() { Value = "cbc"},
                            new FooListItem() { Value = "cbd"},
                            new FooListItem() { Value = "cbe"}
                        }
                    }
                };
    
                serializer.Serialize(writer, foo, namespaces);
    
    
    
            }
        }
        [XmlType(Namespace = "http://example.com")]
        [XmlRoot(Namespace = "http://example.com", IsNullable = false)]
        public partial class Foo
        {
    
            private FooListItem[][] listField;
    
            [XmlArrayItem("Item", IsNullable = false)]
            public FooListItem[][] List
            {
                get
                {
                    return this.listField;
                }
                set
                {
                    this.listField = value;
                }
            }
        }
    
        [XmlType(Namespace = "http://example.com")]
        public partial class FooListItem
        {
    
            private string valueField;
    
            [XmlText]
            public string Value
            {
                get
                {
                    return this.valueField;
                }
                set
                {
                    this.valueField = value;
                }
            }
        }
    }