Code im .NET Framework mithilfe eines XML-Schemas erstellen

Veröffentlicht: 16. Aug 2004 | Aktualisiert: 14. Nov 2004
Von

Anforderungen: Für den Download muss das Microsoft .NET Framework 1.1 installiert sein.

In diesem Artikel lernen Sie die Unterschiede zwischen typisierten DataSets und Klassen kennen, die mit dem Tool xsd.exe generiert wurden. Weiterhin erfahren Sie, wie Sie diesen Codeerstellungs-Prozess durch Wiederverwendung der Infrastrukturklassen erweitern, die diesen Vorgang unterstützen, und gleichzeitig die Kompatibilität mit dem XmlSerializer beibehalten. Dieser Artikel enthält auch Links zu englischsprachigen Seiten.

Laden Sie die dazugehörigen Codebeispiele herunter.

Auf dieser Seite

Einführung
Die grundlegenden Klassen
Erweitern der XSD-Verarbeitung
XmlSerializer
Anpassen mit CodeDom
Zuordnungstipps
Schlussfolgerung

Einführung

Die automatische Codegenerierung kann die Produktivität von Entwicklern extrem steigern, seien es Datenzugriffsschichten, Geschäftsentitätsklassen oder sogar Benutzeroberflächen. Dieser Erstellungsprozess kann auf verschiedenen Eingaben basieren, wie z.B. einer Datenbank, einer beliebigen XML-Datei oder einem UML-Diagramm. Microsoft Visual Studio .NET bietet integrierte Unterstützung für die Coderstellung aus W3C XML-Schemadateien (XSD) in zwei verschiedenen Formen: typisierte DataSets und benutzerdefinierte Klassen für die Verwendung mit XmlSerializer.

XSD-Dateien beschreiben den Inhalt, der in einem XML-Dokument zulässig ist, damit dieser vom Dokument als gültig angesehen wird. Der notwendige typsichere Umgang mit den Daten, die letztendlich als XML für die Verwendung serialisiert werden, führte zu verschiedenen Ansätzen zur Konvertierung der XSD in Klassen. Bedenken Sie, dass XSD nicht als Methode zur Beschreibung von Objekten und ihren Beziehungen geschaffen wurde. Für diesen Zweck gibt es bereits ein besseres Format, UML, das häufig verwendet wird, um Anwendungen zu modellieren und daraus Code zu generieren. Es gibt daher einige (erwartete) Ungereimtheiten zwischen .NET und seinen Konzepten der objektorientierten Programmierung (OOP) und denjenigen von XSD. Bedenken Sie dies, wenn Sie den Weg XSD -> Klassenzuordnung gehen.

In diesem Sinne könnte das CLR-Typsystem als eine Teilmenge von XSD angesehen werden: Es unterstützt Funktionen, die nicht regulären OO-Konzepten zugeordnet werden können. Wenn Sie XSD nur zur Modellierung Ihrer Klassen anstatt zur Modellierung Ihrer Dokumente verwenden, werden Sie fast keine Konflikte finden. Im weiteren Verlauf dieses Artikels wird der Ansatz typisierter DataSets erläutert, wie mit dem Tool xsd.exe generierte benutzerdefinierte Klassen zu einer besseren Lösung führen und wie die Ausgabe der XSD -> Klassenerstellung erweitert und angepasst werden kann.

Sie ziehen den größten Nutzen aus diesem Artikel, wenn Sie über ein grundlegendes Verständnis von CodeDom verfügen.

Welche Nachteile besitzen typisierte DataSets?
Typisierte DataSets werden immer häufiger zur Darstellung von Geschäftsentitäten verwendet, d.h. sie dienen zum Transport von Entitätsdaten zwischen Schichten einer Anwendung oder sogar als Ausgabe von WebServices. Typisierte DataSets sind deshalb interessant, weil Sie dadurch auch typisierten Zugriff auf Tabellen, Zeilen und Spalten usw. erhalten. Jedoch sind diese auch mit gewissen Abstrichen und/oder Einschränkungen verbunden:

  • Zusätzlicher Implementierungsaufwand: DataSets enthalten eine Vielzahl von Funktionen, die für Ihre Entitäten eventuell nicht erforderlich sind, z.B. das Nachverfolgen von Änderungen, SQL-ähnliche Abfragen, Datenansichten und zahlreiche Ereignisse usw.

  • Leistung: Die Serialisierung in/aus XML ist nicht gerade schnell und wird vom XmlSerializer leicht übertroffen.

  • Interoperabilität: Bei Clients von Webservices, die .NET nicht unterstützen, ist die Rückgabe typisierter DataSets eventuell problematisch.

  • XML-Strukturen: Viele hierarchische (und absolut gültige) Dokumente und ihre Schemata lassen sich nicht in einem Tabellenmodell vereinfachen.

Hier erhalten Sie weitere Informationen über typisierte DataSets.

Die Verwendung typisierter DataSets für die Datenübergabe ist eventuell nicht die optimale Lösung, es sei denn, die zusätzlichen Funktionen von DataSets im Allgemeinen sind für Sie bedeutsam. Glücklicherweise gibt es eine weitere Option, die Sie verwenden können.

XmlSerializer und benutzerdefinierte Klassen
Der XmlSerializer verbessert den gesamten Vorgang der XML-Datenverarbeitung. Mithilfe von Serialisierungsattributen ist der XmlSerializer in der Lage, Objekte aus ihren XML-Darstellungen wieder herzustellen und diese wieder zurück in XML zu serialisieren. Darüber hinaus erledigt das Programm diese Aufgabe sehr effizient, da es eine dynamisch kompilierte XmlReader-basierte (und somit Streaming-) Klasse generiert, die speziell auf die (De)Serialisierung des konkreten Typs abzielt. Dies bringt eine extrem schnelle Arbeitsweise mit sich.

Hier erhalten Sie weitere Informationen über XML-Serialisierungsattribute.

Natürlich ist es nicht praktikabel, wenn Sie raten müssen, welche Attribute für die Entsprechung einer XSD zu verwenden sind. Zur Lösung dieses Problems enthält das .NET SDK ein Dienstprogramm, das Ihnen diese Arbeit abnimmt: xsd.exe. Hierbei handelt es sich um eine Befehlszeilenanwendung, die sowohl typisierte DataSets als auch benutzerdefinierte Klassen aus einer XSD-Datei generieren kann. Die benutzerdefinierten Klassen werden mit den entsprechenden XML-Serialisierungsattributen generiert. Somit wird bei der Serialisierung die genaue Übereinstimmung mit dem Schema gewahrt.

Weitere Informationen finden Sie in "Don Box's intro to XSD and the CLR mapping and attributes" (in Englisch).

So weit, so gut. Wir verfügen nun über eine effiziente und leistungsfähige Methode, XML in Objekte zu konvertieren (und umgekehrt) sowie über ein Tool, das die Klassen automatisch generiert. Das Problem liegt darin, dass wir uns manchmal ein Ergebnis wünschen, das sich von dem generierten leicht unterscheidet. Von xsd.exe generierte Klassen können beispielsweise nicht an ein Windows Forms-Raster datengebunden sein, da dies nach Eigenschaften und nicht nach öffentlichen Feldern zur Anzeige sucht. Es ist eventuell wünschenswert, an bestimmten Stellen eigene benutzerdefinierte Attribute hinzuzufügen, oder Arrays in typisierte Auflistungen zu ändern usw. Natürlich sollte dabei die Kompatibilität mit der XSD bei der Serialisierung gewahrt bleiben.

Die Anpassung der XSD führt offensichtlich zu einer Veränderung der generierten Klassen. Wenn Sie lediglich PascalCase in den de facto XML-Standard für die Verwendung von camelCase ändern möchten, würde ich mir das zweimal überlegen. Zukünftige Produkte von Microsoft deuten darauf hin, dass PascalCase zur XML-Darstellung verwendet wird, um diese .NET-freundlicher zu machen.

Welche Möglichkeiten haben Sie, wenn Sie weitere Anpassungen wie die oben genannten benötigen? Es besteht die allgemeine Auffassung, dass sich xsd.exe nicht erweitern oder anpassen lässt. Diese stimmt jedoch nicht, da das .NET XML-Team die Klassen zur Verfügung gestellt hat, die von diesem Tool verwendet werden. Sie müssen sich zwar mit CodeDom befassen, um diese nutzen zu können, die Grenzen der Anpassungsmöglichkeiten werden jedoch nur von Ihren Anforderungen bestimmt!

Weitere Informationen zu CodeDom finden Sie in den folgenden Artikeln:
Dynamisches Generieren und Kompilieren von Quellcode in mehreren Sprachen
Generate .NET Code in Any Language Using CodeDOM (in Englisch)

Die grundlegenden Klassen

Eine (eher monolithische) Methode zur Generierung von Code aus XSD besteht darin, einfach das Schema-Objektmodell (SOM) zu durchlaufen und Code direkt daraus zu erstellen. Dieser Ansatz wird von zahlreichen Codegeneratoren verwendet, die geschaffen wurden, um die Einschränkungen des xsd.exe-Tools zu überwinden. Dieser Ansatz erfordert ein beträchtliches Maß an Aufwand und Code, da u.a. XSD->CLR-Typzuordnungen, XSD-Typenvererbung, XML-Serialisierungsattribute usw. zu berücksichtigen sind. Die Beherrschung des SOM ist zudem keine einfache Aufgabe. Anstatt all dies selbst zu erledigen, wäre es nicht großartig, wenn wir lediglich die integrierte Codegenerierung des xsd.exe-Tools erweitern oder ändern könnten?

Wie bereits oben erwähnt, sind im Gegensatz zur allgemeinen Meinung die eigentlichen Klassen öffentlich, die xsd.exe zur Generierung von Ausgaben verwendet, und im System.Xml.Serialization-Namespace verfügbar, selbst wenn das xsd.exe-Tool selbst sicherlich keine Anpassungen zulässt. Es stimmt, dass diese größtenteils undokumentiert sind, jedoch werde ich in diesem Abschnitt demonstrieren, wie sie sich verwenden lassen. Lassen Sie sich nicht von der folgenden Aussage in der MSDN-Hilfe einschüchtern: "Der Typ [TheTopSecretClassName] unterstützt die Microsoft .NET Framework-Infrastruktur und ist nicht für die direkte Verwendung in Ihrem Code bestimmt". Ich verwende sie, ohne auf Hacks oder irgendwelchen Reflektionscode zurückzugreifen.

Ein viel besserer Ansatz als die relativ gewöhnliche "StringBuilder.Append"-Codegenerierung ist die Nutzung der Klassen im System.CodeDom-Namespace. Und genau das tun die integrierten Klassen zur Codegenerierung (die ab jetzt einfach als codegen bezeichnet werden). Der CodeDom enthält Klassen, die es ermöglichen, fast jedes Programmierkonstrukt sprachunabhängig in einer so genannten abstrakten Syntaxstruktur (AST, abstract syntax tree) darzustellen. Später kann eine weitere Klasse, der Codegenerator, diese interpretieren und daraus den erwarteten Rohcode generieren, z.B. Microsoft Visual C# oder Microsoft Visual Basic.NET. Auf diese Weise wird der Großteil der Codegenerierung im .NET Framework durchgeführt.

Nicht nur der Codegen-Ansatz macht sich dies zunutze, sondern die Schemaanalyse und die tatsächliche CodeDom-Generierung werden ebenfalls über einen Zuordnungsprozess voneinander getrennt. Diese Zuordnung muss für jedes Schemaelement durchgeführt werden, für das Code generiert werden soll. Im Wesentlichen wird dabei ein neues Objekt generiert, das das Ergebnis der Analyse repräsentiert, z.B. seine Struktur, die aus dem Typnamen, der für das Objekt generiert wird, seinen Membern, deren CLR-Typ usw. besteht.

Für die Verwendung dieser Klassen wird folgender einfacher Workflow befolgt:

  1. Laden des Schemas (in der Regel eines).

  2. Ableiten einer Reihe von Zuordnungen für jedes der XSD-Elemente der höheren Ebene.

  3. Exportieren dieser Zuordnungen in einen System.CodeDom.CodeDomNamespace.

Vier Klassen sind in diesen Prozess involviert, die alle im System.Xml.Serialization-Namespace definiert sind:
NETFrameworkundXMLSchema_01
Abbildung 1. Klassen, die bei der Ermittlung einer "CodeDom"-Struktur verwendet werden.


Die Ermittlung einer CodeDom-Struktur mithilfe dieser Klassen lässt sich wie folgt erreichen:

namespace XsdGenerator
{
  public sealed class Processor
  {
    public static CodeNamespace Process( string xsdFile, 
       string targetNamespace )
    {
      // Load the XmlSchema and its collection.
      XmlSchema xsd;
      using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) )
      {
        xsd = XmlSchema.Read( fs, null );
        xsd.Compile( null );
      }
      XmlSchemas schemas = new XmlSchemas();
      schemas.Add( xsd );
      // Create the importer for these schemas.
      XmlSchemaImporter importer = new XmlSchemaImporter( schemas );
      // System.CodeDom namespace for the XmlCodeExporter to put classes in.
      CodeNamespace ns = new CodeNamespace( targetNamespace );
      XmlCodeExporter exporter = new XmlCodeExporter( ns );
      // Iterate schema top-level elements and export code for each.
      foreach ( XmlSchemaElement element in xsd.Elements.Values )
      {
        // Import the mapping first.
        XmlTypeMapping mapping = importer.ImportTypeMapping( 
          element.QualifiedName );
        // Export the code finally.
        exporter.ExportTypeMapping( mapping );
      }
      return ns;
    }
  }
}

Der Code ist relativ einfach, auch wenn Sie an einigen Stellen Code zur Ausnahmeverwaltung hinzufügen können. Beachten Sie, dass der XmlSchemaImporter einen Typ unter Verwendung seines qualifizierten Namens importiert, der sich dann im entsprechenden XmlSchema befindet. Alle globalen Elemente im Schema müssen diesem daher übergeben werden, die mithilfe der XmlSchema.Elements-Auflistung durchlaufen werden. Diese Auflistung sowie XmlSchemaElement.QualifiedName sind Member des so genannten Post-Schema-Validation Infoset (PSVI, siehe MSDN-Hilfe), das nach der Schemakompilierung gefüllt wird. Dies bewirkt, dass die Schemainformationen nach der Auflösung von Verweisen, Schematypen, Vererbung, Inklusionen usw. gefüllt und organisiert werden. Seine Funktionalität entspricht in etwa der des DOM Post Validation Infoset (PSVI, siehe Dare Obasanjo's MSDN article (in Englisch) und XSD specification (in Englisch)).

Sie haben eventuell einen Nebeneffekt (bzw. Nachteil) in der Arbeitsweise des XmlSchemaImporter bemerkt: Sie können nur die Zuordnung für global definierte Elemente abrufen (importieren). Alle anderen Elemente, die lokal an irgendeiner Stelle im Schema definiert sind, sind über diesen Mechanismus nicht zugänglich. Dies hat einige Konsequenzen, die die anwendbaren Anpassungen einschränken oder unser Schemadesign beeinflussen. Diese werden später noch erläutert.

Die XmlCodeExporter-Klasse füllt den CodeDomNamespace mit Typdefinitionen entsprechend den importierten Zuordnungen. Der Namespace wird an den Konstruktor übergeben, wodurch eine so genannte CodeDom-Struktur erstellt wird. Die sich aus der oben erwähnten Methode ergebende CodeDom-Struktur entspricht genau der Struktur, die intern vom xsd.exe-Tool generiert wird. Sobald diese Struktur verfügbar ist, kann sie entweder direkt in eine Assembly kompiliert oder Quellcode generiert werden.

Wenn das xsd.exe-Tool nicht mehr erwünscht ist, lässt sich problemlos eine Konsolenanwendung erstellen, die diese Klasse verwendet. Für diesen Zweck muss eine Quellcodedatei aus der erhaltenen CodeDom-Struktur generiert werden. Dies geschieht durch Erstellung eines CodeDomProvider, der der vom Benutzer ausgewählten Zielsprache entspricht:

static void Main( string[] args )
{
  if ( args.Length != 4 )
  {
    Console.WriteLine(
      "Usage: XsdGenerator xsdfile namespace outputfile [cs|vb]" );
    return;
  }
  // Get the namespace for the schema.
  CodeNamespace ns = Processor.Process( args[0], args[1] );
  // Create the appropriate generator for the language.
  CodeDomProvider provider;
  if ( args[3] == "cs" )
    provider = new Microsoft.CSharp.CSharpCodeProvider();
  else if ( args[3] == "vb" )
    provider = new Microsoft.VisualBasic.VBCodeProvider();
  else
    throw new ArgumentException( "Invalid language", args[3] );
  // Write the code to the output file.
  using ( StreamWriter sw = new StreamWriter( args[2], false ) )
  {
    provider.CreateGenerator().GenerateCodeFromNamespace(
      ns, sw, new CodeGeneratorOptions() );
  }
  Console.WriteLine( "Finished" );
  Console.Read();
}

Die Formatierung des generierten Codes sowie andere Optionen lassen sich problemlos mithilfe der Eigenschaften der CodeGeneratorOptions-Instanz anpassen, die vom Generator entgegengenommen werden. Informationen zu den verfügbaren Optionen finden Sie in der MSDN-Dokumentation.

Nach dem Kompilieren dieser Konsolenanwendung kann Code generiert werden, der exakt dem des xsd.exe-Tools entspricht. Diese Funktionalität ermöglicht eine vollständige Unabhängigkeit von diesem Tool, d.h. es ist nicht mehr notwendig, seinen Installations- bzw. Speicherort zu kennen, oder dafür einen neuen Prozess zu starten, usw. Die ständig wiederholte Ausführung über die Befehlszeile bei jeder Änderung des Schemas ist jedoch keineswegs ideal. Microsoft Visual Studio.NET ermöglicht Entwicklern die Nutzung der Entwurfszeit-Codegenerierung über so genannte benutzerdefinierte Tools. Ein Beispiel ist das typisierte DataSet, bei dem (obwohl keine Angabe erforderlich ist) ein benutzerdefiniertes Tool die XSD-Datei des DataSets bei jeder Speicherung verarbeitet, wodurch automatisch die entsprechende Codebehind-Klasse generiert wird.

Die Erstellung benutzerdefinierter Tools übersteigt den Rahmen dieses Artikels. Jedoch erhalten Sie weitere Informationen über die Umwandlung des bisher erstellten Codes in dem Webprotokollbeitrag XSD -> Classes Generator Custom Tool (radically new and extensible) (in Englisch). Der Code für dieses Tool ist im Artikeldownload enthalten. Sie können diesen verwenden, indem Sie einfach den benutzerdefinierten Toolnamen "XsdCodeGen" einer XSD-Dateieigenschaft zuweisen. Die Registrierung wird in der begleitenden Infodatei erläutert.

Selbst wenn dieses benutzerdefinierte Tool einfacher zu verwenden ist, wirkt es nicht besonders überzeugend, das xsd.exe-Tool mit einem anderen zu ersetzen, das genau die gleichen Funktionen besitzt. Schließlich bestand der ursprüngliche Zweck dieser Übung darin, genau dieses Verhalten zu ändern! Auf dieser Grundlage werden im Anschluss weitere Anpassungen beschrieben.

Erweitern der XSD-Verarbeitung

Für die Anpassung der Verarbeitung müssen Informationen darüber an das Tool übergeben werden, was zu ändern oder zu verarbeiten ist. Es gibt hier zwei primäre Optionen:

  • Hinzufügen von (potenziell vielen) Attributen zum XSD-Stammelement (<xs:schema>), die vom eigenen Prozessor verstanden werden, um Anpassungen anzuwenden, die in etwa dem Ansatz mit typisierten DataSets entsprechen.

    Weitere Informationen dazu finden Sie hier.

  • Verwenden der integrierten XSD-Erweiterbarkeit über Schemaanmerkungen, um beliebige Anpassungen zu ermöglichen. Dabei werden einfach Typen einer Art Codegenerierungs-Pipeline hinzugefügt, die nach Abschluss der grundlegenden Generierung ausgeführt wird.


Der erste Ansatz wirkt zunächst aufgrund seiner Einfachheit attraktiver. Es muss lediglich ein Attribut hinzugefügt und der Prozessor entsprechend geändert werden, um danach zu suchen:

Schema:

<xs:schema elementFormDefault="qualified"  
  xmlns:xsd="<A href="http://www.w3.org/2001/XMLSchema">http://www.w3.org/2001/XMLSchema</A>" 
  xmlns:code="<A href="http://weblogs.asp.net/cazzu">http://weblogs.asp.net/cazzu</A>" 
  code:fieldsToProperties="true">

Code:

XmlSchema xsd;
// Load the XmlSchema.
...
foreach (XmlAttribute attr in xsd.UnhandledAttributes)
{
  if (attr.NamespaceURI == "http://weblogs.asp.net/cazzu")
  {
    switch (attr.LocalName)
    {
      case "fieldsToProperties":
        if (bool.Parse(attr.Value)) ConvertFieldsToProperties(ns);
        break;
      ...
    }
  }
}

Sie werden diesen Ansatz allgemein in anderen xsd->Klassen-Generatoren feststellen (Sie finden eine Vielzahl von diesen im Code Generation Network (in Englisch)). Leider führt dieser Ansatz zu langen switch-Anweisungen, endlosen Attributen und schließlich zu nicht mehr verwaltbarem Code und fehlender Erweiterbarkeit.

Der zweite Ansatz erweist sich als stabiler, da er die Erweiterbarkeit von Anfang an berücksichtigt. XSD bietet eine solche Erweiterungsfunktion über das <xs:annotation>-Element, das fast jedem Element im Schema untergeordnet sein kann. Mithilfe dieses und seines untergeordneten <xs:appinfo>-Elements sollten Entwickler angeben können, welche (beliebige) Erweiterungen auszuführen sind und in welcher Reihenfolge. Ein solches erweitertes Schema sieht in etwa wie folgt aus:

<xs:schema elementFormDefault="qualified"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:annotation>
    <xs:appinfo>
      <Code xmlns="http://weblogs.asp.net/cazzu">
        <Extension 
Type="XsdGenerator.Extensions.FieldsToPropertiesExtension, 
XsdGenerator.CustomTool" />
      </Code>
    </xs:appinfo>
  </xs:annotation>

Natürlich muss jede Erweiterung eine gemeinsame Schnittstelle implementieren, damit das benutzerdefinierte Tool jede einzelne ausführen kann:

public interface ICodeExtension 
{ 
  void Process( System.CodeDom.CodeNamespace code,  
                System.Xml.Schema.XmlSchema schema ); 
}

Indem eine solche Erweiterbarkeit schon von vornherein zur Verfügung gestellt wird, kann bei Auftreten neuer Anpassungsanforderungen leichter reagiert werden. Selbst die einfachsten Anpassungen lassen sich von Anfang an als Erweiterungen implementieren.

Erweiterbares Codeerstellungstool
Zur Ermöglichung dieser neuen Funktion wird einfach die Processor-Klasse geändert und jedes <Extension>-Element aus dem Schema abgerufen. Es gibt dabei jedoch noch einen Vorbehalt: Im Gegensatz zu den PSVI-Eigenschaften, die für Elemente, Attribute und Typen etc. verfügbar sind, gibt es keine typisierte Eigenschaft für Anmerkungen auf der Schemaebene, d.h. es gibt keine XmlSchema.Annotations-Eigenschaft. Für die Suche nach Anmerkungen ist es deshalb erforderlich, die generische Vorkompilierungseigenschaft XmlSchema.Items zu durchlaufen. Zudem muss nach Erkennung eines XmlSchemaAnnotation-Elements die generische Auflistung seiner eigenen Elemente erneut durchlaufen werden, da untergeordnete <xs:appinfo>- und <xs:documentation>-Elemente vorhanden sein können, die wiederum keine typisierte Eigenschaft besitzen. Sobald der Inhalt von appinfo schließlich erreicht wird, erhalten wir über die XmlSchemaAppInfo.Markup-Eigenschaft lediglich ein Array von XmlNode-Objekten. Sie können sich vorstellen, wie es weitergeht: Durchlaufen der Knoten, erneutes Durchlaufen seiner untergeordneten Elemente usw. Dies führt zu ziemlich unschönem Code.

Glücklicherweise ist eine XSD-Datei nicht mehr als eine XML-Datei, weshalb sie mit XPath abgefragt werden kann.

Zur schnelleren Ausführung wird ein statischer kompilierter Ausdruck für den XPath in der Processor-Klasse beibehalten, die in ihrem statischen Konstruktor initialisiert wird.

public sealed class Processor
{
  public const string ExtensionNamespace = "http://weblogs.asp.net/cazzu";
  private static XPathExpression Extensions;
  static Processor() 
  {
    XPathNavigator nav = new XmlDocument().CreateNavigator();
    // Select all extension types.
    Extensions = nav.Compile
    ("/xs:schema/xs:annotation/xs:appinfo/kzu:Code/kzu:Extension/@Type");
    
    // Create and set namespace resolution context.
    XmlNamespaceManager nsmgr = new XmlNamespaceManager(nav.NameTable);
    nsmgr.AddNamespace("xs", XmlSchema.Namespace);
    nsmgr.AddNamespace("kzu", ExtensionNamespace);
    Extensions.SetContext(nsmgr);
  }

Anmerkung Weitere Informationen zu den Vorteilen, Einzelheiten und erweiterten Anwendungen der XPath-Vorkompilierung und -Ausführung finden Sie unter Performant XML (I): Dynamic XPath expressions compilation (in Englisch) und Performant XML (II): XPath execution tips (in Englisch).

Die Process()-Methode muss diese Abfrage durchführen und jeden gefundenen ICodeExtension-Typ unmittelbar vor Rückgabe des CodeNamespace an den Aufrufer ausführen:

XPathNavigator nav; 
using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) ) 
{ nav = new XPathDocument( fs ).CreateNavigator(); } 
XPathNodeIterator it = nav.Select( Extensions ); 
while ( it.MoveNext() ) 
{ 
  Type t = Type.GetType( it.Current.Value, true ); 
  // Is the type an ICodeExtension? 
  Type iface = t.GetInterface( typeof( ICodeExtension ).Name ); 
  if (iface == null) 
    throw new ArgumentException( "Invalid extension type '" +  
 it.Current.Value + "'." ); 
     ICodeExtension ext = ( ICodeExtension ) Activator.CreateInstance( t ); 
     // Run it! 
  ext.Process( ns, xsd ); 
} 
return ns;

Ich verwende Type.GetInterface() zum Überprüfen der Schnittstellenimplementierung anstelle von Type.IsAssignableFrom(), da diese Methode scheinbar über weniger Overhead verfügt und nicht verwalteten Code schneller durchläuft. Der Effekt ist der gleiche, wobei jedoch die Zweite einen Booleschen Wert anstelle eines Typs zurückgibt (oder null, falls keine Schnittstelle gefunden wird).

XmlSerializer

CodeDom bringt für Entwickler, die Anpassungsmöglichkeiten suchen, nicht nur mehr Leistungsfähigkeit und Flexibilität, sondern auch eine größere Verantwortung mit sich. Es besteht das Risiko einer derartigen Veränderung des Codes, dass sich dieser nicht mehr kompatibel zum Schema serialisieren lässt, oder dass die Funktionalität von XmlSerializer gänzlich beschädigt ist, wobei Ausnahmen für unerwartete Knoten und Attribute ausgelöst werden, Werte nicht abgerufen werden können usw.

Es ist daher absolut notwendig, die Funktionsweise des XmlSerializer zu kennen, bevor der generierte Code modifiziert wird. Außerdem sollten Sie wissen, was hinter den Kulissen vorgeht.

Wenn ein Objekt kurz vor seiner XML-Serialisierung steht, wird eine temporäre Assembly durch Reflektieren des Typs erzeugt, den Sie an den XmlSerializer-Konstruktor übergeben (dies ist der Grund, warum dies erforderlich ist). Sie brauchen sich wegen des "Reflektieren"-Ausdrucks keine Sorgen zu machen! Dies geschieht nur einmal pro Typ. Weiterhin wird ein extrem effizientes Paar aus Reader- und Writer-Klassen erstellt, um die Serialisierung und Deserialisierung während der Lebensdauer der AppDomain zu erledigen.

Die Klassen erben die öffentlichen XmlSerializationReader- und XmlSerializationWriter-Klassen im System.Xml.Serialization-Namespace. Hierbei handelt es sich ebenfalls um den Typ [TheTopSecretClassName]. Wenn Sie sich diese dynamisch generierten Klassen ansehen möchten, müssen Sie lediglich die folgende Einstellung zur Anwendungskonfigurationsdatei hinzufügen (web.config für eine Webanwendung):

<system.diagnostics> 
  <switches> 
    <add name="XmlSerialization.Compilation" value="4"/> 
  </switches> 
</system.diagnostics>

Dies bewirkt, dass der Serializer die temporären Dateien nicht mehr löscht, die dabei generiert werden. Für eine Webanwendung befinden sich die Dateien unter C:\Dokumente und Einstellungen\[IhrComputername]\ASPNET\Lokale Einstellungen\Temp; ansonsten befinden sie sich im Ordner Lokale Einstellungen\Temp des aktuellen Benutzers.

Der angezeigte Code entspricht genau der Vorgehensweise, die für ein effizientes Laden von XML in .NET erforderlich ist: Verwendung von verschachtelten While- und If-Konstrukten beim Lesen, Verwendung von XmlReader-Methoden für die Navigation stromabwärts usw. Der ganze unschöne Code dient im Wesentlichen der Steigerung der Geschwindigkeit.

Probleme in diesen generierten Klassen lassen sich auch mithilfe von Chris Sells' XmlSerializerPreCompiler tools (in Englisch) diagnostizieren.

Wir können uns diesen Code ansehen, um die Auswirkung einer Änderung in den vom Serializer generierten Klassen zu analysieren.

Anpassen mit CodeDom

Bei einigen Anpassungen ist der Nutzwert sofort sichtbar, da es sich um häufige Aspekte bei Klassen handelt, die vom xsd.exe-Tool generiert wurden.

Umwandeln von Feldern in Eigenschaften
Eines der Probleme, über die sich die meisten Entwickler beklagen, besteht darin, dass xsd.exe Klassen mit öffentlichen Feldern generiert anstelle von Eigenschaften, deren Grundlage private Felder darstellen. Die vom XmlSerializer generierten Klassen lesen und schreiben Werte aus der Instanz der Klasse mithilfe der regulären [object].[member]-Notation. Natürlich spielt es aus der Sicht der Kompilierung und des Quellcodes keine Rolle, ob [member] ein Feld oder eine Eigenschaft ist.

Es ist also mithilfe von CodeDom möglich, die Standardklassen für die XSD zu ändern. Dank der Erweiterbarkeit, die in das benutzerdefinierte Codegen-Tool integriert ist, muss lediglich eine neue ICodeExtension implementiert werden. Die Erweiterung verarbeitet jeden Typ in der CodeDom-Struktur, falls es sich entweder um eine Klasse oder eine Struktur handelt:

public class FieldsToPropertiesExtension : ICodeExtension
{
  #region ICodeExtension Members
  public void Process( System.CodeDom.CodeNamespace code, 
                       System.Xml.Schema.XmlSchema schema )
  {
    foreach ( CodeTypeDeclaration type in code.Types )
    {
      if ( type.IsClass || type.IsStruct )
      {
         // Turn fields to props

Nun müssen alle Member des Typs durchlaufen werden (es kann sich um Felder, Eigenschaften, Methoden etc. handeln) und nur diejenigen vom Typ CodeMemberField verarbeitet werden. Es ist jedoch nicht einfach möglich, ein foreach-Konstrukt auf die type.Members-Auflistung anzuwenden, da für jedes Feld eine Eigenschaft derselben Auflistung hinzugefügt werden muss. Dies würde zu einer Ausnahme führen, da der zugrunde liegende Enumerator, der vom foreach-Konstrukt verwendet wird, ungültig werden würde. Deshalb müssen die aktuellen Member in ein Array kopiert werden und stattdessen das Array selbst durchlaufen werden:

CodeTypeMember[] members = new CodeTypeMember[type.Members.Count]; 
type.Members.CopyTo( members, 0 ); 
foreach ( CodeTypeMember member in members ) 
{ 
  // Process fields only. 
  if ( member is CodeMemberField ) 
  { 
    // Create property

Als Nächstes wird eine neue Eigenschaft erstellt:

CodeMemberProperty prop = new CodeMemberProperty(); 
prop.Name = member.Name; 
prop.Attributes = member.Attributes; 
prop.Type = ( ( CodeMemberField )member ).Type; 
// Copy attributes from field to the property. 
prop.CustomAttributes.AddRange( member.CustomAttributes ); 
member.CustomAttributes.Clear(); 
// Copy comments from field to the property. 
prop.Comments.AddRange( member.Comments ); 
member.Comments.Clear(); 
// Modify the field. 
member.Attributes = MemberAttributes.Private; 
Char[] letters = member.Name.ToCharArray(); 
letters[0] = Char.ToLower( letters[0] ); 
member.Name = String.Concat( "_", new string( letters ) );

Beachten Sie, dass in die neue Eigenschaft der Feldname, seine Memberattribute und der Typ kopiert werden. Kommentare und die benutzerdefinierten Attribute (die XmlSerialization-Attribute) werden aus dem Feld in die Eigenschaft verschoben (AddRange() und Clear()). Schließlich wird das Feld in ein privates umgewandelt und sein erster Buchstabe klein geschrieben mit vorausgehendem "_"-Zeichen, was eine ziemlich verbreitete Namenskonvention für Felder ist, die die Grundlage von Eigenschaften bilden.

Die wichtigste Komponente in einer Eigenschaft fehlt jedoch immer noch: Ihre get- und set-Accessor-Implementierungen. Da es sich dabei einfach um Übergaben an den Feldwert handelt, sind diese relativ unkompliziert:

prop.HasGet = true; 
prop.HasSet = true; 
// Add get/set statements pointing to field. Generates: 
// return this._fieldname; 
prop.GetStatements.Add(  
  new CodeMethodReturnStatement(  
    new CodeFieldReferenceExpression(  
      new CodeThisReferenceExpression(), member.Name ) ) ); 
// Generates: 
// this._fieldname = value; 
prop.SetStatements.Add( 
  new CodeAssignStatement( 
    new CodeFieldReferenceExpression(  
      new CodeThisReferenceExpression(), member.Name ),  
    new CodeArgumentReferenceExpression( "value" ) ) );

Schließlich wird die neue Eigenschaft dem Typ hinzugefügt:

type.Members.Add( prop ); 
}

Im Anschluss das Beispiel eines früheren Schemas, das folgende Ausgabe mit dem Tool generiert:

/// <remarks/> 
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] 
public class Publisher 
{ 
  /// <remarks/> 
  public string pub_id;

Nach dem Hinzufügen der entsprechenden Erweiterung zum Schema:

<xs:schema elementFormDefault="qualified" 
xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:annotation>
    <xs:appinfo>
      <Code xmlns="http://weblogs.asp.net/cazzu">
        <Extension 
Type="XsdGenerator.Extensions.FieldsToPropertiesExtension,
       XsdGenerator.CustomTool" /> 
      </Code>
    </xs:appinfo>
  </xs:annotation>
  ...

wird Folgendes generiert:

/// <remarks/> 
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] 
public class Publisher 
{ 
  private string _pub_id; 
  /// <remarks/> 
  public string pub_id 
  { 
    get 
    { 
      return this._pub_id; 
    } 
    set 
    { 
      this._pub_id = value; 
    } 
  }

Verwenden von Auflistungen anstelle von Arrays
Damit ein Lesen-/Schreiben-Objektmodell (mit get- und set-Eigenschaften) halbwegs programmiererfreundlich ist, sollten seine mehrwertigen Eigenschaften auf Auflistungen und nicht auf Arrays basieren. Dies erleichtert eine Bearbeitung der Werte und eine Änderung der Objektgrafik. Dieser reguläre Ansatz enthält die Ableitung einer neuen typisierten Auflistungsklasse von CollectionBase.

Vor einer endgültigen Änderung von CodeDom muss überprüft werden, inwieweit XmlSerializer Auflistungen unterstützt. Tief im Inneren der Klassen, die den zu serialisierenden Typ analysieren und reflektieren, gibt es eine interne Klasse mit der Bezeichnung TypeScope. Es ist die Aufgabe von TypeScope, sicherzustellen, dass der Serialisierungscode generiert werden kann. Sie enthält eine interessante Methode, ImportTypeDesc, die die meisten Prüfungen durchführt und Informationen für unterstützte Typen erstellt. Hier findet sich auch die spezifische Unterstützung für IXmlSerializable (sucht nach Sicherheitsattributen in seinen Membern), Arrays (müssen eine Rangordnung gleich 1 besitzen), Enums, XmlNode, XmlAttribute sowie XmlElement, etc.

Speziell für Auflistungen sucht die import-Methode nach Typen, die ICollection implementieren. Dabei müssen folgende Regeln befolgt werden:

  • Sie müssen über eine Add-Methode verfügen, die nicht von der Schnittstelle definiert ist, da diese in der Regel für den speziellen Typ erstellt wird, den die Auflistung beinhaltet.

  • Sie dürfen nicht IDictionary durch die Auflistung implementieren.

  • Sie müssen über einen Standard-Member verfügen (z.B. einen Indexer) mit einem einzelnen Parameter vom Typ System.Int32 (C# int). Nach einem solchen Member wird in der gesamten Typhierarchie gesucht.

  • Sie dürfen über keine Sicherheitsattribute in Add, Count und im Indexer verfügen.

Nach Überprüfung dieser Informationen verwendet die generierte spezielle Klasse, die von XmlSerializationWriter abgeleitet ist, die Count-Eigenschaft zum Durchlaufen anstelle der Length-Eigenschaft für arraybasierte Eigenschaften, während die XML-Ausgabe für unseren Typ geschrieben wird:

MyAssembly.MyCollection a = (MyAssembly.MyCollection)o.@CollectionProperty;
if (a != null) {
    for (int ia = 0; ia < a.Count; ia++) {
        Write10_MyCollectionItem(@"MyCollectionItem", 
          @"http://weblogs.asp.net/cazzu/", 
          ((MyAssembly.MyCollectionItem)a[ia]), false, false);
    }
}

Beachten Sie, dass bei der vorausgehenden Prüfung des Indexers der indizierte Zugriff auf eine Auflistung und einen Array gleich ist. Somit gibt es hier keine Änderungen.

Die entsprechende, von XmlSerializationReader abgeleitete Klasse verwendet die typisierte Add-Methode, um die Auflistung aufzufüllen.

MyAssembly.MyCollection a_2 = (MyAssembly.MyCollection)o.@CollectionProperty;
...
while (Reader.NodeType != System.Xml.XmlNodeType.EndElement) 
{
  if (Reader.NodeType == System.Xml.XmlNodeType.Element) 
  {
    if (((object) Reader.LocalName == (object)id8_MyCollectionItem && 
       (object) Reader.NamespaceURI == (object)id9_httpweblogsaspnetcazzu)) 
    {
      if ((object)(a_2) == null) 
        Reader.Skip(); 
      else 
        a_2.Add(Read10_MyCollectionItem(false, true));
    }
    ...

Die oben gezeigte read-Methode gibt den entsprechenden Typ zurück, den die Auflistung erwartet:

MyAssembly.MyCollectionItem Read1_MyCollectionItem(bool isNullable,  
  bool checkType)

Nachdem nun die Unterstützung von XmlSerializer für auflistungsbasierte Eigenschaften und sein korrekter Umgang damit überprüft wurde, ist es sicher, alle Arrays in die entsprechenden stark typisierten Auflistungen umzuwandeln.

Diese neue Erweiterung kann für die Ausführung vor oder nach der vorhergehenden bestimmt sein. Dieser Unterschied ist von Bedeutung, da die Iteration von Feldern zu den entsprechenden neuen Eigenschaften wechseln würde. Um diese Erweiterung von der vorhergehenden unabhängig zu machen, wird der Code für den Umgang mit Feldern ausgerichtet. Beachten Sie jedoch, dass der Code inkorrekt ist, wenn diese Erweiterung für eine Ausführung NACH der FieldsToPropertiesExtension konfiguriert wird.

Als Erstes wird die Methode analysiert, mit der die benutzerdefinierte Auflistung erstellt wird. Die Auflistung sollte wie folgt aussehen:

public class PublisherCollection : CollectionBase 
{ 
  public int Add(Publisher value) 
  { 
    return base.InnerList.Add(value); 
  } 
  public Publisher this[int idx] 
  { 
    get { return (Publisher) base.InnerList[idx]; } 
    set { base.InnerList[idx] = value; } 
  } 
}

Der Code für die Erstellung dieser typisierten Auflistung wird nachfolgend aufgeführt:

public CodeTypeDeclaration GetCollection( CodeTypeReference forType )
{
  CodeTypeDeclaration col = new CodeTypeDeclaration( 
    forType.BaseType + "Collection" );
  col.BaseTypes.Add(typeof(CollectionBase));
  col.Attributes = MemberAttributes.Final | MemberAttributes.Public;
  // Add method
  CodeMemberMethod add = new CodeMemberMethod();
  add.Attributes = MemberAttributes.Final | MemberAttributes.Public;
  add.Name = "Add";
  add.ReturnType = new CodeTypeReference(typeof(int));
  add.Parameters.Add( new CodeParameterDeclarationExpression (
    forType, "value" ) );
  // Generates: return base.InnerList.Add(value);
  add.Statements.Add( new CodeMethodReturnStatement (
    new CodeMethodInvokeExpression( 
      new CodePropertyReferenceExpression( 
        new CodeBaseReferenceExpression(), "InnerList"), 
      "Add", 
      new CodeExpression[] 
        { new CodeArgumentReferenceExpression( "value" ) } 
      )
    )
  );
  // Add to type.
  col.Members.Add(add);
  // Indexer property ('this')
  CodeMemberProperty indexer = new CodeMemberProperty();
  indexer.Attributes = MemberAttributes.Final | MemberAttributes.Public;
  indexer.Name = "Item";
  indexer.Type = forType;
  indexer.Parameters.Add( new CodeParameterDeclarationExpression (
    typeof( int ), "idx" ) );
  indexer.HasGet = true;
  indexer.HasSet = true;
  // Generates: return (theType) base.InnerList[idx];
  indexer.GetStatements.Add( 
    new CodeMethodReturnStatement (
      new CodeCastExpression( 
        forType, 
        new CodeIndexerExpression( 
          new CodePropertyReferenceExpression( 
            new CodeBaseReferenceExpression(), 
            "InnerList"), 
          new CodeExpression[] 
            { new CodeArgumentReferenceExpression( "idx" ) } ) 
        )
      )
    );
  // Generates: base.InnerList[idx] = value;
  indexer.SetStatements.Add( 
    new CodeAssignStatement( 
      new CodeIndexerExpression( 
        new CodePropertyReferenceExpression( 
          new CodeBaseReferenceExpression(), 
          "InnerList"), 
        new CodeExpression[] 
          { new CodeArgumentReferenceExpression("idx") }), 
      new CodeArgumentReferenceExpression( "value" )
    )
  );
  // Add to type.
  col.Members.Add(indexer);
  return col;
}

An dieser Stelle sollten Sie einen nützlichen Tipp berücksichtigen, wenn Sie innerhalb des CodeDom programmieren; sind Ihnen diese scheinbar endlosen Statements.Add-Zeilen aufgefallen? Natürlich hätten wir diese in einzelnen Zeilen anordnen können, wobei jede eine temporäre Variable für das Objekt erstellt und an die nächste weitergibt. Das würde sie jedoch noch endloser machen! Wenn Sie sich einmal daran gewöhnt haben, bietet der folgende Tipp eine gute Möglichkeit, diese Zeilen aufzuteilen:
Zur Generierung von verschachtelten CodeDom-Anweisungen werden zusammenhängende Eigenschafts-/Indexer-/Methodenzugriffe in der Regel von rechts nach links konstruiert.

In der Praxis sieht das so aus: Zur Generierung von

base.InnerList[idx]

beginnen Sie beim Indexerausdruck [idx], fahren mit dem Eigenschaftszugriff InnerList fort und hören mit der Objektreferenzbasis auf. Dies ergibt die folgende verschachtelte CodeDom-Anweisung:

CodeExpression st = new CodeIndexerExpression(  
  new CodePropertyReferenceExpression(  
    new CodeBaseReferenceExpression(),  
    "InnerList" 
  ),  
  new CodeExpression[]  
    { new CodeArgumentReferenceExpression( "idx" ) }  
);

Beachten Sie, dass die Anweisung von rechts nach links erstellt wurde und schließlich die entsprechenden Konstruktorparameter abgeschlossen wurden. Es ist in der Regel sinnvoll, die Zeilen manuell so einzuziehen und zu trennen, dass sich leichter erkennen lässt, wo jeder Objektkonstruktor endet und welche Parameter zu ihm gehören.

Schließlich beinhaltet die Implementierung der ICodeExtension.Process-Methode das Durchlaufen von Typen und ihrer Felder, um solche zu finden, die arraybasiert sind:

public class ArraysToCollectionsExtension : ICodeExtension
{
  public void Process( CodeNamespace code, XmlSchema schema )
  {
    // Copy as we will be adding types.
    CodeTypeDeclaration[] types = 
      new CodeTypeDeclaration[code.Types.Count];
    code.Types.CopyTo( types, 0 );
    foreach ( CodeTypeDeclaration type in types )
    {
      if ( type.IsClass || type.IsStruct )
      {
        foreach ( CodeTypeMember member in type.Members )
        {
          // Process fields only.
          if ( member is CodeMemberField && 
            ( ( CodeMemberField )member ).Type.ArrayElementType != null )
          {
            CodeMemberField field = ( CodeMemberField ) member;
            CodeTypeDeclaration col = GetCollection( 
              field.Type.ArrayElementType );
            // Change field type to collection.
            field.Type = new CodeTypeReference( col.Name );
            code.Types.Add( col );
          }
        }
      }
    }
  }

Wie weiter oben wurde die zu ändernde Auflistung kopiert, in diesem Fall CodeNamespace.Types.

Weitere Anpassungsmöglichkeiten sind u.a.: Hinzufügen von [Serializable] zu generierten Klassen; Hinzufügen von DAL-Methoden (d.h. LoadById, FindByKey, Save, Delete, usw.); Generieren von Membern, die bei der Serialisierung ignoriert werden und von Ihrem Code verwendet werden (durch Anwendung von XmlIgnoreAttribute); Nicht-Generierung von Klassen, die zu externen importierten Schemas gehören etc.

Zuordnungstipps

Falls Sie sich eingehender mit dem Codegenerierungstool selbst befassen oder die Schemaverarbeitung noch weiter anpassen möchten, sind die folgenden fortgeschrittenen Themen zu Codegen-Klassen eventuell für Sie von Interesse. Falls Sie nur Erweiterungen entwickeln und den CodeDom ändern möchten, bieten diese Informationen fast keinen zusätzlichen Nutzwert. Sie können diesen Abschnitt ruhig überspringen.

Bei diesem Vorgang wurden Elemente durch Abrufen ihrer XmlTypeMapping verarbeitet. Es wurden dabei keine ihrer Eigenschaften verwendet, obwohl Sie diese eventuell benötigen, wenn Sie CodeTypeDeclaration für ein bestimmtes Element finden müssen. Informationen zu XmlTypeMapping-Eigenschaften und eine kurze Beschreibung ihrer Bedeutung finden Sie in der MSDN-Dokumentation. Diese Klasse wird jedoch in einer Vielzahl von Szenarien verwendet, wie z.B. die in der Dokumentation gezeigten SoapReflectionImporter-Zuordnungsimporte. Bei dem von mir verwendeten XmlSchemaImporter konnte ich feststellen, dass XmlTypeMapping.TypeFullName und XmlTypeMapping.TypeName bei einem bestimmten Schemaelement-Entwurf nicht korrekt funktionieren: Wenn diese ein einzelnes nicht gebundenes, untergeordnetes Element innerhalb einer Sequenz enthält, nehmen beide fälschlicherweise den Typ der untergeordneten Eigenschaft an.

Für das nachfolgende Schemaelement

<xs:element name="pubs"> 
  <xs:complexType> 
    <xs:sequence> 
      <xs:element name="publishers" type="Publisher" maxOccurs="unbounded" /> 
    </xs:sequence> 
  </xs:complexType> 
</xs:element>

gilt Folgendes; Anstelle des "pubs"-Werts, bei dem es sich um den zu generierenden Typ handelt, haben XmlTypeMapping.TypeFullName und XmlTypeMapping.TypeName den Wert "Publisher[]", der zugleich der Typ ihrer einzigen Eigenschaft ist. Wenn die Sequenz mehr als ein Element enthält, funktioniert alles wie erwartet. Beachten Sie, dass dieser (scheinbare) Fehler immer zutrifft, unabhängig davon, ob der Typ des Elements ein benannter globaler Typ ist oder nicht, oder ob das Element selbst ein Verweis ist oder nicht.

Neben der Typzuordnung kann der XmlSchemaImporter auch die Zuordnungen abrufen, die für seine Member (Felder) gelten. Dies ist äußerst nützlich, da die XSD/CLR-Typzuordnungen einschließlich der benutzerdefinierten abgeleiteten Typen der XSD aufgelöst werden, und Sie sich sicher sein können, dass diese für die Verwendung durch den XmlSerializer geeignet sind. Sie können die Memberzuordnungen wie folgt abrufen:

XmlMembersMapping mmap = importer.ImportMembersMapping(  
  element.QualifiedName ); 
int count = mmap.Count; 
for (int i = 0; i < count; i++) 
{ 
  XmlMemberMapping map = mmap[i]; 
  //You have now:  
  //  map.ElementName 
  //  map.MemberName 
  //  map.TypeFullName 
  //  map.TypeName 
}

XmlMemberMapping.TypeFullName enthält den namespacequalifizierten CLR-Typ und XmlMemberMapping.TypeName den XSD-Typnamen. Zum Beispiel hat erstere für ein Member des XSD-Typs "xs:positiveInteger" den Inhalt "System.String" und letztere "positiveInteger". Wenn Sie keinen Zugriff auf diese Abrufmöglichkeit der Memberzuordnung hätten, müssten Sie alle XSD-zu-CLR-Typkonvertierungsregeln kennen, die von XmlSerializer verwendet werden. Beachten Sie, dass diese Regeln nicht unbedingt auch für die XSD-Validierung und DOM PSVI gelten.

Es gibt einen wichtigen Vorbehalt (wiederum wahrscheinlich ein Fehler) beim Importieren von Membern. Sie können den XmlSchemaImporter nicht wieder verwenden, da ansonsten vom importierenden Code eine InvalidCastException ausgelöst wird, in etwa zur Erstellungszeit von XmlMembersMapping. Dieses Problem lässt sich lösen, indem jedes Mal eine neue Instanz des Importers verwendet wird.

Mithilfe dieser Informationen können Sie die Darstellung der Klasse vollständig ändern, z.B. durch Umbenennen der Eigenschaften, um den ersten Buchstaben groß zu schreiben, ohne die Serialisierungsinfrastruktur zu gefährden.

Bei der Diskussion der Basis von Codegen-Klassen wurde erwähnt, dass die Zuordnung für global definierte Elemente nur abgerufen (importiert) werden kann. Wenn Sie Ihre eigenen benutzerdefinierten Attribute erstellen, um die sich daraus ergebenden Klassen zu ändern, können Sie diese nur für die Elemente der höheren Ebene abrufen und analysieren, da Sie nur über die Zuordnungen für diese verfügen. Angenommen, Sie fügen ein code:className-Attribut hinzu, das von einer Erweiterung zur Änderung des generierten Klassennamens verwendet wird:

<xs:schema xmlns:code="http://weblogs.asp.net/cazzu" ...>
  <xs:element name="pubs" code:className="PublishersData">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="publishers" code:className="Publishers">
          <xs:complexType>

Sie können zwar die Zuordnungen für das pubs-Element abrufen, jedoch nicht für das untergeordnete publishers-Element. Eine Verarbeitung wäre nicht sicher, da sich die Codegen-Klassen in der Zukunft ändern können. Ohne die entsprechende Zuordnung können Sie nicht einfach annehmen, dass die CodeTypeDeclaration den gleichen Namen hat wie das Element (um es zu finden und zu ändern). Sie können jedoch das Risiko als akzeptabel ansehen.

Schlussfolgerung

Die Wiederverwendung der integrierten Codegenerierungsfunktionen, die für den XmlSerializer erstellt wurden, stellt sicher, dass geringfügige Änderungen am generierten Code die XML-Serialisierung nicht beschädigen. Eine direkte Änderung seiner Ausgabe über CodeDom stellt ebenfalls ein Plus an Flexibilität dar. In diesem Artikel wurde gezeigt, wie eine flexible Verarbeitung von XML-Schemas beliebigen Erweiterungen eine Änderung der Ausgabe ermöglicht. Außerdem wurden einige nützliche Beispiele entwickelt.

Mit diesem fundierten Basiswissen können Sie sich nun fortgeschritteneren Szenarien zuwenden; externe (importierte/enthaltene) XSD-Schemata und ihre Beziehung zur Codegenerierung Änderung der Codeausgabe zur Wiederverwendung von Typdefinitionen (sowohl XSD- als auch die entsprechenden generierten .NET-Typen) in anwendungsspezifischen oder unternehmensweiten Repositorys, etc.

Ich hoffe, dieser Artikel enthält Denkanstöße für neue Ansätze im Bereich XSD-Speicherung und -verwaltung sowie für die entsprechende Codegenerierung und -wiederverwendung.

Download

Codebeispiele


Anzeigen: