Verschiedene Versionen von XML/XSD Dateien mit einem codezweig verarbeiten

  • C#
  • .NET (FX) 4.0

Es gibt 13 Antworten in diesem Thema. Der letzte Beitrag () ist von stepper71.

    Verschiedene Versionen von XML/XSD Dateien mit einem codezweig verarbeiten

    Hallo,

    ich habe xml Dateien die zu verschiedenen Versionen von XSD chemas gehören.
    Z.B. Datei A = XSD Schema 2.1; Datei B= Schema 3.0, Datei C= Schema 3.1, etc...

    Die Schemas unterscheiden sich nur in Details, der grundsätzliche Aufbau ist gleich.

    Zur Verarbeitung habe ich aus den XSD Schemas Klassen generiert und jeweils in mein Projekt eingebunden.
    Die Verarbeitung für eine XML Datei "xmlHPR" mit der Version 2.1 würde z.B. so aussehen:

    C#-Quellcode

    1. //Deserilisation
    2. ser = new XmlSerializer(typeof(Harvester21.HarvestedProductionType));
    3. var itm = (Harvester21.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    4. //Daten verabeiten...
    5. int anzahlBaumarten = itm.Machine[0].SpeciesGroupDefinition.Length;
    6. int anzahlSortimente = itm.Machine[0].ProductDefinition.Length;
    7. etc....


    Um jetzt die verschiedenen Versionen abzufangen hatte ich mir etwas in dieser Richtung vorgestellt:

    C#-Quellcode

    1. XmlSerializer ser = null;
    2. switch (xmlVersion)
    3. {
    4. case "2.1":
    5. ser = new XmlSerializer(typeof(Harvester21.HarvestedProductionType));
    6. var itm = (Harvester21.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    7. break;
    8. case "3.0":
    9. ser = new XmlSerializer(typeof(Harvester30.HarvestedProductionType));
    10. var itm = (Harvester30.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    11. break;
    12. case "3.1":
    13. ser = new XmlSerializer(typeof(Harvester31.HarvestedProductionType));
    14. var itm = (Harvester31.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    15. break;
    16. }
    17. //Jetzt hier den Code zur Verarbeitung für alle Versionen gleich
    18. int anzahlBaumarten = itm.Machine[0].SpeciesGroupDefinition.Length;
    19. int anzahlSortimente = itm.Machine[0].ProductDefinition.Length;


    Dies funktioniert natürlich so nicht (mehrmals Variable definiert und "itm" steht außerhalb der Bedingung nicht zur Verfügung).
    Hat jemand eine Idee wie ich es vermeide für jede Version den Code der Verarbeitung zu Duplizieren?

    Bin für jede Hilfe dankbar.
    Gruß
    Horten
    @stepper71 Pack die Deklaration der Variable nach außerhalb des switch-Blocks, deklariere sie genau ein Mal und verwende sie wie gehabt.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Ich würde versuchen, mir einen HarvestedProductionType zu basteln, der alle Schemata unterstützt.

    Alternativ (ohne zu behaupten, dass es sauber ist): Du könntest spät binden. Ist aber ineffizienter und kann zur Laufzeit crashen, wenn irgendwas nicht stimmt.
    Geht seit C# 4 mit dem dynamic-Keyword. blogs.msdn.microsoft.com/cburrows/2008/10/27/c-dynamic/

    Und klar, deklarieren musst du natürlich außerhalb des switch-Blockes.
    @RodFromGermany
    Genau das bekomme ich nicht hin.
    Würde ich dies machen:

    C#-Quellcode

    1. XmlSerializer ser = null;
    2. Harvester21.HarvestedProductionType itm = null;
    3. switch (xmlVersion)....


    würde ich mich wieder auf eine Version festlegen, und irgendeine Version von HarvesterProductionType muss ich zur deklartion nehmen.

    @BjöNi
    Die Klassen sind sehr umfangreich (über 100 paritelle Klassen), das wäre ich mir nicht sicher wie ich daraus eine Klasse für alle passend bauen soll.
    Deinen anderen Vorschlag mit dem späte binden überblicke ich noch nicht so richtig, werde versuchen mich da mal einzulesen.

    stepper71 schrieb:

    (mehrmals Variable definiert und "itm" steht außerhalb der Bedingung nicht zur Verfügung).
    Von itm war die Rede, nicht von ser.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    @RodFromGermany: In seinem Fall ist aber ​Harvester21.HarvestedProductionType != Harvester30.HarvestedProductionType
    Das ist eben das grundsätzliche Problem, weswegen ich vorgeschlagen hatte, das neu zu konzipieren (und als imo unsaubere Alternative dynamic vorgeschlagen hatte, falls das nicht möglich ist). ser ist ja eigentlich völlig egal, ob er das jetzt außerhalb oder innerhalb oder gar nicht deklariert und direkt verwendet.
    Erstmal vielen Dank für Eure Antworten.

    @BjöNi hat recht, mein Prolbem ist das ich itm nicht deklarieren kann ohne mich auf eine bestimmte Klasse festzulegen.

    Ich habe den Vorschlag mit der Alternative dynamic ausprobiert und es funktioniert soweit.
    Ist aber wie von @BjöNi angedeutet ein Ritt auf der Rasierklinge....und ich bin mir nicht sicher ob man für solchen Code nicht in die Hölle kommt.
    Von der Vorgehensweise würde ich den Code mit einer bestimmten Klasse schreiben um Schreibfehler zu vermeiden und danach umstellen auf dynamic.

    Der Vollständigkeit halber hier noch der aktuelle Code:

    C#-Quellcode

    1. XmlSerializer ser = null;
    2. dynamic itm= null;
    3. switch (xmlVersion)
    4. {
    5. case "2.1":
    6. ser = new XmlSerializer(typeof(Harvester21.HarvestedProductionType));
    7. itm = (Harvester21.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    8. break;
    9. case "3.0":
    10. ser = new XmlSerializer(typeof(Harvester30.HarvestedProductionType));
    11. itm = (Harvester30.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    12. break;
    13. case "3.1":
    14. ser = new XmlSerializer(typeof(Harvester31.HarvestedProductionType));
    15. itm = (Harvester31.HarvestedProductionType)ser.Deserialize(new StringReader(xmlHPR.OuterXml));
    16. break;
    17. }
    18. int anzahlBaumarten = itm.Machine[0].SpeciesGroupDefinition.Length;
    19. int anzahlSortimente = itm.Machine[0].ProductDefinition.Length;


    Als Alternative würde ich derzeit nur das klassische "durchkurbeln" der XML Dateien ohne eine Deserilisation sehen. Was ich aber auch nicht für sehr elegant halte.




    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „stepper71“ ()

    Die Harvester*-Klassen sind vermutlich aus den Schemata automatisch generiert. Ein Interface aus deren Überschneidungen zu basteln ist aufwendig. Ein leeres Interface ist jedoch vollkommen ausreichend:

    C#-Quellcode

    1. interface IHarvester { }
    2. class Harvester21 {
    3. class HarvestedProductionType : IHarvester {
    4. // etc...


    C#-Quellcode

    1. IHarvester itm;
    2. switch (...) {
    3. itm = ...
    4. }


    Nachteil: Das Interface hat weder Methoden noch Eigenschaften. Vor dem Elementzugriff muss daher die tatsächliche Version bestimmt werden (typeof). Ideal wäre es, wenn sich die Versionen nur durch hinzugefügte Elemente unterscheiden. Dann kannst du ein Interface-Schema wie im alten COM fahren, bei dem sich Interfaces in einer Hierarchie voneinander ableiten. In diesem Fall kannst du die Klassen aber auch direkt voneinander erben lassen und die doppelten Teile entfernen. Um die beste Strategie zu finden, müssten wir die Definitionen der Harvester-Klassen sehen, um deren Versionen vergleichen zu können. Bitte lade sie als Dateianhang hoch, falls möglich.
    Gruß
    hal2000
    Danke das Ihr Euch die Sachen nochmal angucken wollt.

    Ja, die Klassen wurden mit dem XSD Tool aus den Schemas generiert.
    Ich habe die Klassen und die ursprünglichen Schemas hochgeladen.
    Aufgrund des Umfang stimme ich der Einschätzung von @hal2000 zu das eine manuelle Bearbeitung zu Aufwendig ist.

    Zu beachten wäre auch noch, das pro Jahr ein bis zwei zusätzliche Schemas/Klassen eingebunden werden müssen (Version 3.2, 4.0, 4.1, etc....)

    Da ich mit der Verwendung von Interfaces nicht vertraut bin, wäre noch die Frage ob nachträgliche cast auch dort notwendig sind.
    Trotz der Deserilisation sind bisher im nachfolden Code casts wie dieser notwendig:

    C#-Quellcode

    1. var sortimentDaten = sortimentdefinition.Item as Harvester21.ClassifiedProductDefinitionMachineHarvestedProductionType;


    was ich derzeit mit dynamic und einer switch Abfrage abfange.
    Dateien
    Ich habe mir die Klassen mal angesehen. Unter der Voraussetzung, dass sich die Schemata von Version zu Version beliebig ändern können, kannst du den streng typisierten Zugriff eigentlich vergessen. Mit jeder weiteren Version können neue Typen dazukommen oder alte wegfallen - dementsprechend muss mit jeder neuen Version auch das zugreifende Programm grundlegend angepasst werden, auch wenn sich z.B. nur ein Klassenname ändert. Dieses Vorgehen wird m.E. nach mehreren Iterationen im Chaos enden.

    Mir fallen nur zwei sinnvolle Alternativen ein: LINQ to XML (gibts nur in VB.NET!) oder XPATH. In VB kannst du das Schema in der gewünschten Version einfach importieren (wie using in C#). Danach navigierst du in XML-Dateien direkt mit Abfragen wie doc.<node>.<childNode>...<allChildren>.@attribute. Letztere Abfrage würde dir eine Liste der Attributwerte von "attribute" in allen Vorkommen des Knotens "allChildren" unterhalb von node --> childNode liefern. Das hat den Vorteil, dass sich der Aufwand zur Anpassung an eine neue Version auf das Einbinden eines neuen Schemas und der Anpassung aller Abfragen im Code hinausläuft. Diese Variante eignet sich gut, wenn die Struktur der Datengrundlage im Großen und Ganzen stabil bleibt, aber ggf. neue Abfragen dazukommen.

    Für dich besser geeignet ist XPATH: Dort sind alle Abfragen als String definiert. Du kannst sie notfalls extern ändern (sofern sie als ApplicationSetting definiert sind oder in irgendeiner Datei stehen). Sollten sich zwischen Versionen z.B. Klassennamen ändern, genügt die Anpassung der Abfragen. XPATH ist besser geeignet, wenn sich die Struktur der Datengrundlage häufig ändert, weil die Anpassung der Abfragen so einfach ist.

    Ich kann dir für beide Fälle eine Beispielabfrage schreiben, wenn du eine XML-Datei zur Verfügung stellst, die einem der bereits hochgeladenen Schemata genügt. Falls das nicht geht, kannst du dir auch im MSDN Beispiele zu XPATH ansehen:
    msdn.microsoft.com/en-us/library/ms256115(v=vs.110).aspx
    msdn.microsoft.com/en-us/library/ms256086(v=vs.110).aspx

    Beide Vorschläge entsprechen natürlich dem "Durchkurbeln", wie du es nennst, der XML-Dateien. Durch die Versionierung ist das aber unvermeidbar. Strenge Typisierung ist nur dann sinnvoll, wenn sich das Schema nicht ändert.

    Gruß
    hal2000

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „hal2000“ ()

    Vielen Dank @hal2000 für deine Ausführlichen Anmerkungen.
    VB.NET würde nicht in Frage kommen.

    Mit XPATH habe ich bisher nicht gearbeitet, werde ich mir aber mal etwas genauer ansehen. Ich habe eine einfache XML Datei der Version 2.1 hochgeladen, für Beispiele wäre ich natürlich sehr dankbar.
    Eine typische Frage an die Beispieldatei wäre: Alle LogVolume Werte für das Attribut logVolumeCategory="m3 (price)" aus dem Knoten Machine - Stem [...] - SingleTreeProcessedStem- Log [...]

    Die Grundsätzliche Struktur mit den für mich wichtigen Datenfeldern wird sich "voraussichtlich" nicht ändern, da diese sehr fundamentale Inhalte beschreiben.
    Ein typisierter Zugriff wäre mir natürlich lieber (vor allem mit dem Hintergrund das einem ständig auf die Finger gehauen wird wenn man es nicht tut... ;-)), wenn es aber nicht sinnvoll ist bin ich offen für Alternativen.
    Dateien

    stepper71 schrieb:

    Eine typische Frage an die Beispieldatei wäre: Alle LogVolume Werte für das Attribut logVolumeCategory="m3 (price)" aus dem Knoten Machine - Stem [...] - SingleTreeProcessedStem- Log [...]

    Kein Problem - in VB würdest du schreiben:

    VB.NET-Quellcode

    1. Dim d As XDocument = XDocument.Load("Harvester2.1.xml")
    2. ' Die erste (.First) von allen (...) Maschinen
    3. Dim firstMachine = d.<HarvestedProduction>...<Machine>.First()
    4. ' Alle (...) Stems, die ein (.) <SingleTreeProcessedStem> enthalten
    5. Dim stpStems = firstMachine...<Stem>.<SingleTreeProcessedStem>
    6. ' Aus dem ersten SingleTreeProcessedStem alle (...) <Log>-Elemente
    7. Dim allLogs = stpStems.First()...<Log>
    8. ' Von jedem <Log>-Element der Liste alle (...) <LogVolume>-Elemente
    9. Dim allLogVolumes = allLogs...<LogVolume>
    10. ' Nimm von allen <LogVolume>-Elementen, deren Attribut logVolumeCategory == "m3 (price)" ist, den Wert.
    11. Dim result = From lv In allLogVolumes Where lv.@logVolumeCategory = "m3 (price)" Select lv.Value

    Das Ergebnis ist die Liste [0.1077, 0.0931, 0.1223, 0.038] (Werte des ersten STP-Stems).

    Oder als Einzeiler für alle Werte:

    VB.NET-Quellcode

    1. Dim result = From lv In d.<HarvestedProduction>...<Machine>.First()...<Stem>.<SingleTreeProcessedStem>...<Log>...<LogVolume> Where lv.@logVolumeCategory = "m3 (price)" Select lv.Value


    Und hier nochmal als XPATH:

    VB.NET-Quellcode

    1. Dim doc = New XPath.XPathDocument("Harvester2.1.xml")
    2. Dim nav = doc.CreateNavigator()
    3. Dim mgr As New XmlNamespaceManager(nav.NameTable)
    4. mgr.AddNamespace("x", "urn:skogforsk:stanford2010") ' Default-Namespace als 'x' definieren
    5. Dim expr = nav.Compile("x:HarvestedProduction/x:Machine[1]/x:Stem[1]/x:SingleTreeProcessedStem/x:Log/x:LogVolume[@logVolumeCategory=""m3 (price)""]")
    6. expr.SetContext(mgr)
    7. Dim result = nav.Select(expr)
    8. If expr.ReturnType = XPath.XPathResultType.NodeSet Then
    9. For Each node In result
    10. Console.WriteLine(node)
    11. Console.WriteLine("-------------")
    12. Next
    13. End If

    Die Übersetzung in C# dürfte nicht allzu schwer sein. Wie oben: Um die Werte für alle Stems zu bekommen, einfach die [1] an x:Stem[1] entfernen. Wichtig und unvermeidbar ist die Definition des Namespaces. Haben die Selektoren der XPATH-Query kein Namespace-Präfix (hier willkürlich als x gewählt), definiert der W3C-Standard, dass im leeren Namespace und nicht im Standardnamespace gesucht wird. Da dein Dokument aber einen Standardnamespace definiert, würde der Navigator ohne das x und den NamespaceManager keine Ergebnisse liefern. Siehe dazu auch: stackoverflow.com/questions/42…manager-not-working-as-ex
    Gruß
    hal2000