XSD.exe /c - Klassengenerierung richtig gemacht

Published Samstag, 26. Juni 2010 15:07
Einleitung:
XSD.exe hat so seine Tücken, ich glaube das zeigt auch der große Unmut, der im Internet zu finden ist. Dabei muss das doch nicht sein Big Smile

Ich habe festgestellt, dass wenn man XSD verwenden möchte, dass es sinnvoll ist den Berg zum Propheten zu tragen. Im Folgenden erkläre ich, was das Problem eigentlich so dabei ist, und wie man das geschickt umschiffen kann.

Problembeschreibung:
XSD generiert lediglich aus den Teilen Klassen und Felder, von denen es denkt, dass diese zu gebrauchen sind. Dabei beachtet es nur die Elemente in den Schemas, Complex- und SimpleTypes für sich betrachtet lässt es außen vor.

Beispiel:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein SimpleElement, das ebenfalls in der Klassengeneration beachtet werden soll -->
  <xs:simpleType name="SimpleRootElement">
    <xs:restriction base="xs:string">
      <xs:enumeration value="TestVal1" />
      <xs:enumeration value="TestVal2" />
    </xs:restriction>
  </xs:simpleType>

</xs:schema>

Hier meldet XSD die Fehlermeldung
Warning: cannot generate classes because no top-level elements with complex type were found.

Genauso sieht es aus, wenn das Schema so aussehen würde:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />

</xs:schema>


Und wiederum folgendes Schema, das ebenfalls die o.g. Fehlermeldung provoziert:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />
  
  <!-- Ein ComplexType, aus dem eine Klasse generiert werden soll -->
  <xs:complexType name="ComplexTypeWhichIsReferenced">
    <xs:sequence>
      <xs:element name="CommonInnerElement1" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

</xs:schema>


Was will das "doofe" XSD denn überhaupt, warum erzeugt es mir aus dem Schema keine Klassen?

Problemerklärung:
XSD will zwingend ein ElementType, das im Root des Xml-Baumes des Schemas liegt. Weiterhin will es zwingend mindestens einen ComplexType definiert wissen, welches von solch einem Root-Element referenziert wird. Also folgendes Schema ist als Basis für XSD ausreichend, so dass es mit einer Klassengenerierung beginnt:

<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es allerdings ein Top level element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced2' veweist-->
  <xs:element name="TestElement2" type="ComplexTypeWhichIsReferenced2" />
  <xs:complexType name="ComplexTypeWhichIsReferenced2">
    <xs:sequence>
      <xs:element name="CommonInnerElement2" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

</xs:schema>


Ergebnis (Kommentare und Attribute entfernt):
public partial class ComplexTypeWhichIsReferenced2 {
    
    private string commonInnerElement2Field;
    
    /// <remarks/>
    public string CommonInnerElement2 {
        get {
            return this.commonInnerElement2Field;
        }
        set {
            this.commonInnerElement2Field = value;
        }
    }
}


Na das ist doch soweit super, oder? Jetzt hat XSD endlich ein Ergebnis produziert, das auch einem prüfenden Blick Stand hält.
Aber...

Erweiterte Problembeschreibung:
Wenn jetzt Kombinationen aus den oben genannten Schemas und deren Fehlermeldungen mit dem funktionierenden Beispiel zusammenspielen, dann wird es gefährlich. Denn dann wird XSD keine Fehlermeldung ausspucken und aber nur einen Teil für die Klassengenerierung heranziehen. Das ist denke ich das größte Manko, das diesem Tool angelastet wird. So denkt der Benutzer, dass das Tool eine gewisse undurchschaubare Eigenintelligenz verwendet.

Ich habe hier einmal ein kleines Schema entwickelt, das sehr viele Probleme auf einmal betrachtet:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein SimpleElement, das ebenfalls in der Klassengeneration beachtet werden soll -->
  <xs:simpleType name="SimpleRootElement">
    <xs:restriction base="xs:string">
      <xs:enumeration value="TestVal1" />
      <xs:enumeration value="TestVal2" />
    </xs:restriction>
  </xs:simpleType>
  
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />
  
  <!-- Ein ComplexType, aus dem eine Klasse generiert werden soll -->
  <xs:complexType name="ComplexTypeWhichIsReferenced">
    <xs:sequence>
      <xs:element name="CommonInnerElement1" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es allerdings ein Top level element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced2' veweist-->
  <xs:element name="TestElement2" type="ComplexTypeWhichIsReferenced2" />
  <xs:complexType name="ComplexTypeWhichIsReferenced2">
    <xs:sequence>
      <xs:element name="CommonInnerElement2" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es ein inneres Element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced' veweist-->
  <xs:complexType name="TestInnerLink">
    <xs:sequence>
      <xs:element name="ComplexInnerElement" type="ComplexTypeWhichIsReferenced" />
    </xs:sequence>
  </xs:complexType>
</xs:schema>


Wenn man das so mittels XSD durch den Klassengenerator jagt, dann entsteht folgender C# Code:
public partial class ComplexTypeWhichIsReferenced2 {
    
    private string commonInnerElement2Field;
    
    /// <remarks/>
    public string CommonInnerElement2 {
        get {
            return this.commonInnerElement2Field;
        }
        set {
            this.commonInnerElement2Field = value;
        }
    }
}


Was unschwer zu erkennen ist: Das ist genau das Gleiche, wie in dem oben genannten funktionierenden Minischema. Also hier hat XSD alles andere als das TestElement2 ignoriert!

Lösung:
Die Lösung ist aus der oben erwähnten Fehlermeldung heraus begründet. XSD betrachtet nur den Inhalt zur Klassengenerierung, welcher als Root-Element auf andere Complex-Typen verweist.
Das könnte man nun manuell machen, oder es automatisiert anwenden. Ich habe hier ein kleines Tool geschrieben, das die notwendigen Elemente einfach dem Schema hinzufügt. Die Logik im Schema ändert sich quasi nicht, aber XSD ist glücklich und das Ergebnis sieht sehr viel besser aus.
Das Tool macht von der Logik her folgendes (Codeausschnitt):
        /// <summary>
        /// Methode, die ein XML Schema erweitert, so dass keine ClassType ohne Element existieren,
        /// das darauf verweist.
        /// </summary>
        /// <remarks>Verhindert die Warnung:
        /// cannot generate classes because no top-level elements with complex type were found.</remarks>
        /// <param name="xsdFullFilename">Der voll qualifizierte Dateiname zur XSD Schemadatei.</param>
        /// <param name="throwError">Definiert, ob im Fehlerfall eine Exception geworfen werden soll.</param>
        /// <returns>True, wenn die Erweiterung der Schemadatei erfolgreich war, andernfalls False.</returns>
        public static bool GenerateDummyElementsForClassTypes(string xsdFullFilename, bool throwError)
        {
            XmlDocument schemaDoc = null;
            bool retVal = false;
            List<string> rootElements = new List<string>();
            Dictionary<string, string> rootElementsToAddToSchema = new Dictionary<string, string>();

            try
            {
                // Überprüfung, ob Schemadatei vorhanden
                if (!System.IO.File.Exists(xsdFullFilename))
                {
                    throw new System.IO.FileNotFoundException(String.Format("Konnte die Datei '{0}' nicht finden!", xsdFullFilename));
                }

                // Versuch das Schema in ein XmlDokument zu laden
                schemaDoc = new XmlDocument();
                schemaDoc.Load(xsdFullFilename);

                // Alle Root-Elements herausfinden
                ExtractAllElements(schemaDoc, rootElements);

                // Alle Root-ComplexTypes herausfinden
                ExtractAllClassTypes(schemaDoc, rootElements, rootElementsToAddToSchema);

                // Alle vorgemerkten Root-Elements dem Schema hinzufügen
                AddNecessaryRootElementsToSchemafile(xsdFullFilename, schemaDoc, rootElementsToAddToSchema);

                retVal = true;
            }
            catch
            {
                // wenn gewünscht, dann Fehler schmeißen, ansonsten nur den
                // Rückgabewert auf "False" setzen.
                if (throwError)
                {
                    throw;
                }
                retVal = false;
            }

            return retVal;
        }

Es gibt einen vollständigen Download des VisualStudio 2010 Projektes hier: XsdPrepare.zip

Durch Anwendung der Methode GenerateDummyElementsForClassTypes entsteht aus dem kompletten Schema oben folgendes:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- Ein SimpleElement, das ebenfalls in der Klassengeneration beachtet werden soll -->
  <xs:simpleType name="SimpleRootElement">
    <xs:restriction base="xs:string">
      <xs:enumeration value="TestVal1" />
      <xs:enumeration value="TestVal2" />
    </xs:restriction>
  </xs:simpleType>
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />
  <!-- Ein ComplexType, aus dem eine Klasse generiert werden soll -->
  <xs:complexType name="ComplexTypeWhichIsReferenced">
    <xs:sequence>
      <xs:element name="CommonInnerElement1" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es allerdings ein Top level element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced2' veweist-->
  <xs:element name="TestElement2" type="ComplexTypeWhichIsReferenced2" />
  <xs:complexType name="ComplexTypeWhichIsReferenced2">
    <xs:sequence>
      <xs:element name="CommonInnerElement2" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es ein inneres Element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced' veweist-->
  <xs:complexType name="TestInnerLink">
    <xs:sequence>
      <xs:element name="ComplexInnerElement" type="ComplexTypeWhichIsReferenced" />
    </xs:sequence>
  </xs:complexType>
  <xs:element name="RootElement0" type="SimpleRootElement" />
  <xs:element name="RootElement1" type="TestInnerLink" />
</xs:schema>


Und wenn man dann XSD nochmal drüberlaufen lässt, dann entsteht folgender Code:
public partial class ComplexTypeWhichIsReferenced2 {
    
    private string commonInnerElement2Field;
    
    /// <remarks/>
    public string CommonInnerElement2 {
        get {
            return this.commonInnerElement2Field;
        }
        set {
            this.commonInnerElement2Field = value;
        }
    }
}

public partial class ComplexTypeWhichIsReferenced {
    
    private string commonInnerElement1Field;
    
    /// <remarks/>
    public string CommonInnerElement1 {
        get {
            return this.commonInnerElement1Field;
        }
        set {
            this.commonInnerElement1Field = value;
        }
    }
}

public enum SimpleRootElement {
    
    /// <remarks/>
    TestVal1,
    
    /// <remarks/>
    TestVal2,
}

public partial class TestInnerLink {
    
    private ComplexTypeWhichIsReferenced complexInnerElementField;
    
    /// <remarks/>
    public ComplexTypeWhichIsReferenced ComplexInnerElement {
        get {
            return this.complexInnerElementField;
        }
        set {
            this.complexInnerElementField = value;
        }
    }
}


Wenn man jetzt ein letztes Mal genau drauf schaut, dann hat XSD den Fall CommonRootElement nicht betrachtet, aber hier muss man zu dessen Schutz sagen, hier wüsste ich auch nicht, wie man hieraus eine Klasse basteln soll.

Aber mit meinem kleinen Helfertool gehen nun (hoffentlich) keine Inhalte mehr durch die Klassengenerierung verloren, und so kann man das auch in automatisierten Prozessen dann einsetzen.

Ich hoffe, dass die nähere Betrachtung hilfreich war, und dass ihr etwas mit dem Tool anfangen könnt. Über Kommentare und Verbesserungsvorschläge würde ich mich freuen.

Es gibt einen vollständigen Download des VisualStudio 2010 Projektes hier: XsdPrepare.zip

Kommentare

# dotnet-kicks.de said on Samstag, 26. Juni 2010 22:56

Sie wurden gekickt (eine gute Sache) - Trackback von dotnet-kicks.de

# Maria said on Mittwoch, 7. September 2011 17:05

Hi,

guter Beitrag! Leider finde ich im Netz viele Anleitungen darüber, wie ich mit xsd.exe arbeiten soll, aber wenig was ich alles damit erreichen kann, bzw. wann ich es benutzen soll. Vielleicht kannst Du mir mit einem Hinweis helfen? Was ich machen will ist, VISIO 2010 Inhalte(Diagramme) in einem bestimmten XML Format auszugeben. Angenohmen ich generiere aus der Microsofts visio14.xsd  C# Klassen. Was kann ich damit erreichen, ich brauche ja keine Klassen, sondern XML Output? Oder kann/soll man aus den Klassen(oder aus den Klasseninstanzen??) wiederum XML Dateien erzeugen?

# Timo Rehl said on Mittwoch, 28. September 2011 05:38

Hallo Maria,

tut mir leid für die späte Meldung, das geht teilweise bei dem vielen Spam unter.

XSD.exe ist wirklich nur dafür da, um aus einem XML-Schema C# Klassen zu generieren.

So ganz ist mir Dein Anliegen aber nicht klar? Was für Visio Diagramme sind das (UML?), und wieso benötigst Du dann XML daraus?

Viele Grüße

Timo

Kommentar abgeben

(verpflichtend) 
(verpflichtend) 
(optional)
(verpflichtend)