Accendo / Publikované

Webové služby a XSD v .Net (základ)

Autor:  Libor Bešenyi

Dátum:  26.1.2013

 

V tomto poste by som sa chcel venovať schémam – nie ich opisu, ale už praktickými príkladmi v .Net.

XSD schéma popisuje formát XML. SOAP je postavený na WSDL, kde XSD popisuje interface služby. Keďže VS si vie naimportovať WSDL (Reference.cs), musia existovať nejaké nástroje v tomto prípade utilitka XSD.exe, ktorej praktické použitie si vysvetlíme na nasledujúcich riadkoch. Prosím pozor, jedná sa o inšpiráciu – musí existovať milión spôsobov ako riešiť tu popisované problémy!

Ako už bolo spomenuté, schémy sa najčastejšie používajú pri B2B komunikácii. Ak máme nejakú službu, ktorá konzumuje dáta v určitom formáte, tak v zásade máme dve možnosti. Buď publikujeme štruktúry a SOAP / WSDL sa postará o XSD, alebo si vygenerujeme XSD ručne a vstupom do služby bude napr. byte[] so serializovaným XML. Výhoda prvého postupu je, že je to rýchle. Nevýhodou je to, že ak chceme odtieniť rozhranie (aby každá zmena v kóde neovplyvnila vonkajší interface), musíme aj tak mať dve sady štruktúr a vytvorené mapovanie medzi nimi.

Príklad z praxe: nevhodný návrh cez WSDL

Prečo je dôležité odtienenie? Nedávno som riešil problém zlej implementácie webovej služby.

Existoval centrálny SOAP uzol a potom zopár oddelených aplikácii. Keďže sa k systémom pristupovalo cez SSO (užívateľ sa prihlási z jedného miesta a „skáče“ po systémoch bez toho, aby vedel, že ho práve obsluhuje úplne iný systém). Existoval tu samozrejme nejaký systém práv (riadený práve z centrálneho uzla). Jednotlivé aplikácie si pri prvom skoku do aplikácie opýtali informácie o užívateľovi. Potiaľ je všetko super.

Problém nastal, keď sa do systému pridal nový druh aplikácie a s ním nový „typ“ prístupových práv. Typ prístupových práv bola obyčajná enumerácia. Centrálny uzol začal teda publikovať nový druh prístupových práv (ktoré nijako nesúviseli s už existujúcimi). No práve to, že sa rozšírila enumerácia a staré systémy nemali updatnuté WSDL (klientská strana), tak užívateľom, ktorí mali prístup aj do nového systému začal centrálny uzol posielať aj nové práva do starých aplikácii, ktoré ich ale nepoznali. Nový typ prístupových práv teda odpálil všetky staré systémy lebo SOAP vrstva sa snažila namapovať novú enumeráciu, ktorú ale nepoznala.

Riešením je samozrejme len úbohý update WSDL (kedy sa naimportuje aj rozšírená enumerácia). No ak je tých systémov veľa, asi by to vedenie nepotešilo (ak sú priveľmi zošnurované pravidlá).

V mojom prípade som urobil novú službu, ktorá namiesto WSDL publikuje to isté ale v podobe serializovaných dát. Starej službe som ale zakázal publikovať nový druh práv (aj tak ju používajú staré systémy, ktoré tieto práva nevyužijú). Starú službu som označil ako obsolentnú a postupne keď sa bude robiť údržba v týchto systémov, budem ich prerábať aj na použitie novej služby (až ju nakoniec odstránim).

Takže záver je ten, že ak si nedáme pozor, WSDL môže spôsobiť odpálenie externých systémov. Horšie to je, ak tieto systémy sú u zákazníka. Práve serializácia vie zabrániť podobným problémom, ale nie je taká pohodlná, ako import WSDL.

Riešenie a lá „Rest“

Ako by teda vyzeralo riešenie postavené na serializácii? Na strane servera dosť podobne. Spokojne tam môžeme mať štruktúry a aj enumeráciu s typom práv. Lenže služba nebude publikovať štruktúru, ale pole bajtov (alebo string) v ktorom budeme túto štruktúru serializovať.

Toto riešenie silne kopíruje myšlienky REST technológie, kde namiesto služby by bol len nejaký handler, ktorý vracia http response napr. typu xml. Klient potom musí ručne načítať XML a spracovať ho, podľa potreby. Filozofia RESTu sa mi pozdáva, ale nie za každú cenu musíme naše systémy postaviť na RESTe. Hlavne ak rozprávame o existujúcej infraštruktúre – podľa mňa nemá zmysel bezhlavo sa vrhať na REST, stačí si z neho zobrať myšlienku.

Takže ako vidíme, server sa takmer nemení. Horšie to už je s klientom. Klient už nedostane rozserializovanú štruktúru, ale suché XML. Preto je toto riešenie pracnejšie z hľadiska počiatočných nákladov. Samozrejme spracovanie XML môžeme urobiť tak, že budeme deserializovať XML do ručne vytvoreného „Reference.cs“ (viď nižšie). Niekedy je to vhodné, ak hovoríme o komplexnejších štruktúrach, napr. json konverzia do XML apod.

Alebo ak sa jedná práve o dáta ako prístupové práva, je podľa mňa vhodnejšie zapúzdriť túto komunikáciu do novej triedy, ktorá ako vstup zoberie XML, ktoré potom parsuje a vracia len tie informácie, ktoré aplikácia skutočne potrebuje. Teda napr. v zozname Access rights by hľadala len tie, ktoré sa týkajú danej aplikácie (enumerácia je text, takže neznámy text sa proste preskočí).

Záleží teda na charaktere úlohy, ako upraviť klienta.

Príklad (de)serializácia

Ďalej sa nebudeme teda venovať WSDL, ale serializácii v .Net. Ja som veľa článkov o tom nenašiel (na rozdiel od WSDL), tak aj preto som sa rozhodol o tom písať. Najprv si predveďme ako urobiť z inštancie nejakej triedy XML.

Každá trieda, ktorú chceme publikovať musí mať zadefinovaný atribút Serializable, príkladom môže byť:

public enum Enum1

{

        Unknown,

        Pes,

        Macka,

        Krava

}

 

[Serializable]

public class Demo1

{

        public Enum1 EnumField;

        public string StringField;

}

 

Ako vytvoriť z tejto triedy a tejto jej inštancie Xml?

var instancia = new Interface.Demo1()

{

EnumField = Interface.Enum1.Krava,

StringField = "Malina"

};

 

Napíšeme si metódu, ktorá využije .Net serializátor:

public static XDocument Save(object value)

{

        var document = new XDocument();

        var xmlSerializer = new XmlSerializer(value.GetType());

        using (var xmlWriter = document.CreateWriter())

                xmlSerializer.Serialize(xmlWriter, value);

 

        return document;

}

 

Vďaka tejto metóde dokážeme serializovať náš objekt volaním Save(instancia).ToString() pričom získame takéto XML:

<Demo1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

  <EnumField>Krava</EnumField>

  <StringField>Malina</StringField>

</Demo1>

 

Samozrejme musíme vedieť aj podobné XML vyskladať späť do štruktúry, čo vieme urobiť napr. touto metódou:

public static T Load<T>(XContainer element)

{

        var xmlSerializer = new XmlSerializer(typeof(T));

        using (var xmlReader = element.CreateReader())

                return (T)xmlSerializer.Deserialize(xmlReader);

}

 

Použitie je potom následovné: var instancia = Load<Trieda>(xml). Samozrejme deserializovaná inštancia nemá nič spoločné s inštanciou serializácie (rovnaké sú len dáta). Takže teraz vieme zaserializovať štruktúru napr. v jednom systéme a vyskladať ju opäť v inom.

Serializácia sa nemusí používať len v B2B. Môže byť využitá ako import / export apod. Veď B2B je len automatizovaný prenos dát medzi systémami, ale princíp je rovnaky, ako vyexportovať dáta z jedného systému a naimprotovať do druhého.

POZOR: Save a Load použitím XDocument nepodporuje (de)serializáciu base 64. Teda ak trieda obsahuje binárne dáta, tieto metódy zdochnú. Riešením je prepísanie týchto metód do staršieho XmlDocumentu, ktorý base 64 podporuje.

Generujeme schému z kódu

Dobre, teraz vieme prenášať dáta medzi systémami pomocou XML. Ako však popísať formát našej triedy tretej strane? Práve na to slúži XSD. XSD popíše, ako môže vyzerať XML. Ako som spomínal, určite existujú mnohé lepšie spôsoby ako generovať schému z kódu, ja používam XSD.EXE utilitku, ktorá je súčasťou Visual Studia.

Pre zaujímavosť, si ale rozšírme našu triedu o nullable int field:

[Serializable]

public class Demo1

{

        public Enum1 EnumField;

        public string StringField;

        public int? IntField;

}

 

XSD.EXE vygeneruje z tejto triedy Scheme0.XSD súbor týmto CMD príkazom:

XSD {Cesta k DLL / EXE} /type:[NameSpace.]{Trieda}

Pozrime sa čo sa to tam teda vygenerovalo:

<?xml version="1.0" encoding="utf-8"?>

<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="Demo1" nillable="true" type="Demo1" />

  <xs:complexType name="Demo1">

    <xs:sequence>

      <xs:element minOccurs="1" maxOccurs="1" name="EnumField" type="Enum1" />

      <xs:element minOccurs="0" maxOccurs="1" name="StringField" type="xs:string" />

      <xs:element minOccurs="1" maxOccurs="1" name="IntField" nillable="true" type="xs:int" />

    </xs:sequence>

  </xs:complexType>

  <xs:simpleType name="Enum1">

    <xs:restriction base="xs:string">

      <xs:enumeration value="Unknown" />

      <xs:enumeration value="Pes" />

      <xs:enumeration value="Macka" />

      <xs:enumeration value="Krava" />

    </xs:restriction>

  </xs:simpleType>

</xs:schema>

 

Ako môžeme vidieť, toto XSD popisuje našu triedu – názov fieldov a pri enumeráciach dokonca aj „dovolené“ hodnoty.

Takto vygenerované XSD by sme mohli teda poslať programátorom firmy, ktorá by mala naučiť ich software konzumovať naše XMLko, ale aby v danom formáte XMLka posielali do nášho systému.

Bežne sa stáva, že publikovaná štruktúra sa aj bežne v kóde používa. Refaktoringom môže omylom programátor zmeniť názov a v podstate to znamená, že ak sa táto zmena neohlási tretím stranám, alebo neupravia interné systémy, služba môže PRE NICH prestať fungovať (a tie telefonáty zvyknú byť nepríjemne).

Takto vieme napísať UnitTest, ktorý generuje XSD a porovnáva voči starému interfacu. Okamžite, keď je zmena, unit test zakričí chybu a programátor musí sám vyhodnotiť, či zmena v interface bola nutná, či ohrozuje iné systémy (pridanie nového fieldu môže byť spätne kompatibilné) apod. Potom sa buď zmena revertne, alebo upravia sa aj zvyšné systémy. Takto sa dá celkom dobre ustrážiť rozhranie služieb!

Validácia XML proti XSD

Bežne sa stávajú situácie, kedy sa zmení schéma na strane servera a klientovi[1] sa to „zabudne“ povedať. Ten stále generuje dáta v starom formáte, no náš systém už očakáva nový formát. Predstavme si, že kedysi v našej enumerácii bola položka „Kon“. Teraz sme ju vyhodili, no klientska aplikácia ju tam vygeneruje (to vieme odchytávať práve porovnávaním XSD schém v unit testingu – viď predošlá kapitola).

Niektoré schémy sú riadne komplikované. Riešil som komunikáciu so štátnou správou[2], kde sa mali posielať nejaké údaje do centrálneho registra. V tomto prípade som písal klienta. Ich server ešte nebol dotiahnutý (certifikáty, legislatíva apod.), ale formát dát bol jasný. Aby sme nemuseli čakať na štátnu správu, z našich dát sme začali pomaly generovať XML, ktoré bude ich server konzumovať. Aby sme overovali formát, použili sme práve ich schémy. Opäť .Net ponúka riešenie:

public static List<ValidationEventArgs> Validate(this XDocument document, string targetNamespace, string schemaUri)

{

        var result = new List<ValidationEventArgs>();

        var schemaSet = new XmlSchemaSet();

        schemaSet.Add(targetNamespace, schemaUri);

        document.Validate(schemaSet, (o, e) => result.Add(e));

        return result;

}

 

Takže teraz môžeme skúsiť zvalidovať XML, ktoré sa nám zoserializuje. Samozrejme musí byť valídne, pretože aj schému aj dané XML generujeme z tých istých zdrojákov:

var instancia = new Interface.Demo1() { EnumField = Interface.Enum1.Krava, StringField = "Malina" };

var xml = XmlUtils.Save(instancia);

 

var validacneChyby = XmlUtils.Validate(xml, /*nameSpace*/null, /*schema*/@"Schemy\Demo1.xsd");

MessageBox.Show("Chyb: " + validacneChyby.Count);

 

Skúsme si zvalidovať „ručne“ vyrobené XML:

xml = XDocument.Parse("<Demo1><EnumField>Krava</EnumField><StringField /><IntField>1</IntField></Demo1>");

 

validacneChyby = XmlUtils.Validate(xml, /*nameSpace*/null, /*schema*/@"Schemy\Demo1.xsd");

MessageBox.Show("Chyb: " + validacneChyby.Count);

 

A teraz XML, ktoré ma nejakú chybu. Napr. posiela pre enum field spomínanú chybnú hodnotu „Kon“. Okamzite dostávame chybové hlásenie:

The 'EnumField' element is invalid - The value 'KON' is invalid according to its datatype 'Enum1' - The Enumeration constraint failed.

Nill kontra NULL

Tak, doteraz šlo všetko hladko. No ako to býva zvykom (hlavne pri M$ technológiách), v okamihu, kde začíname mať vyššie nároky, technológia nás necháva v štichu. Nie náhodou som pridal do štruktúry NULL integer. NULLable fieldy sa často pridávajú, lebo majú zázračnú vlastnosť, ak je zmena spätne kompatibilná, integrujúca aplikácia nemusí aplikovať nové schémy (ak to nevyžaduje business  logika samozrejme).

Tu ale nastáva problém. Asi z historických dôvodov (.Net 1.1 neobsahoval NULLable fieldy) sa po ich implementácii musela urobiť zmena spätne kompatibilná s v1.1. A tak nám namiesto NULL XML hodnôt pribudol atribút NIL. Alebo možno len v M$ chceli mať možnosť riadiť mandatory fieldy, ktoré môžu byť NULLable.

V XMLku ak sa element nepoužije, resp. je uzavretý s prázdnou hodnotou (<Field />), jedná sa o NULL hodnotu. Vyskúšajme si ale z validovať takéto XML nad našou schémou (deserializovať pôjde, len nevieme použiť schémy na overenie formátu):

var xml = XDocument.Parse("<Demo1><EnumField>Krava</EnumField></Demo1>");

 

var deserializovanaInstancia = XmlUtils.Load<Interface.Demo1>(xml);

 

var validacneChyby = XmlUtils.Validate(xml, /*nameSpace*/null, /*schema*/@"Schemy\Demo1.xsd");

MessageBox.Show("Chyb: " + string.Join(" | ", validacneChyby.Select(item => item.Message)));

 

Takže napriek tomu, že XML je možné deserializovať (kde chýbajúce hodnoty sú NULL) – takéto XML nie je valídne voči schéme, ktorú sme vygenerovali!

To teda znamená, že XSD.EXE síce generuje schému z kódu, ale nie tak, ako je zvykom. Pozrime sa bližšie na NULLable int v schéme:

      <xs:element minOccurs="1" maxOccurs="1" name="IntField" nillable="true" type="xs:int" />

 

Tu vidíme, že napriek tomu, že field je NULLable, schéma ho bude očakávať, lebo minimálny výskyt v XML je 1. Opraviť to je jednoduché, napísal som malú open-source utilitku, ktorá sa dá stiahnúť odtiaľ: http://accendo.sk/Download/NillToMinOccurs.zip

Pri generovaní XSD sa výsledné XSD preženie ešte utilitkou (parametrom je len názov súboru) a dostaneme XSD ktoré NULLable fieldy nepovažuje za povinné.

Generovanie štruktúry z XSD

Ako som spomínal, niekedy je na strane klienta namiesto tvorby XML vhodnejšie mapovať interné položky. Ak je napr. štruktúra dosť komplexná a vieme o nej, že v každom systéme musí byť v jednom čase vždy rovnaká, nemusíme sa trápiť s XMLkami, máme dve možnosti:

-          Publikovať štruktúru z DLL a túto DLL potom používať v systémoch. Potom vlastne vieme deserializátoru poslať typ na danú štruktúru – v zásade naše demo to robí. Keďže tá istá aplikácia obsahuje aj spomínanú triedu, vieme ju deserializovať jedoducho.

 

-          Generovať štruktúru z XSD. Ak je to však štruktúra tretej strany, pravdepodobne nám pošle iba XSD súbor (veď systém môže byť napísaný v Jave). Ak sa nám nechce serinkať s XML, môžeme si teda vyskladať danú triedu, ktorú použijeme pri deserializácii. Tento prístup je praktický totožný s WSDL – pretože zakaždým, keď sa zmení formát, musíme vygenerovať nový CS súbor k novému XSD.

Ako teda na to? Opäť vieme použiť utilitku XSD. Tentokrát, z XSD vytvorí CS týmto príkazom:

XSD [Súbor] /classes

Z Demo1.XSD teda tento príkaz vytvorí CS súbor Demo1.CS ktoré vieme inklúdnuť do projektu a používať na deserializáciu ako našu pôvodnú triedu!

Zaujímavejšie je ale pozrieť čo sa stane s XSD ktoré nepoužíva NIL. Môžeme vidieť, že XSD vytvorilo CS súbor, kde pribudla property IntFieldSpecified (bool). IntField je ale not null! Toto môže spôsobovať problémy, pretože takáto schéma, či už upravená naším systémom, alebo generovana z Java servera naimportuje CS ináč ako očakávame.

Takže tak, ako sme pred tým pregenerovali schému, aby validovala takéto XMLka, teraz ju potrebujeme akoby vrátiť späť. Môžeme použiť tú istú utilitku, len pridajme druhý parameter –r:

NillToMinOccurs [XSD] –r

Namespaces

Ak naše štruktúry sú komplexnejšie a využívame rôzne menne priestory (budem to radšej nazývať namespace), XSD utilitka vie pekne identifikovať rozdiel. Pre každý namespace vytvorí nový XSD súbor a je potom na nás urobiť z tohto výstupu poriadok. Nasimulovať si to môžeme tak, že vynútime iný namespace pre už existujúcu enumeráciu:

[Serializable]

public class DemoNS

{

        [XmlElement(Namespace = "nejmspejs")]

        public Enum1 Enum;

}

 

Máme teda dva XSD súbory, s ktorými nemôžeme pracovať bez ďalšieho zásahu. Pozrime sa prečo. „Nulté“ XSD je to hlavné. Správne ma zadefinované to, že bude využívať ďalší namespace:

  <xs:import namespace="nejmspejs" />

No okamžite ako použijeme schému, zakape to na tejto hláške:

The 'nejmspejs:Enum' element is not declared.

Schému môžeme samozrejme ručne opraviť. Musíme len povedať, v ktorom súbore sa daná definícia nachádza, napr. takto:

<xs:import namespace="nejmspejs" schemaLocation="DemoNS2.xsd" />

Okamžite dane XML prechádza validáciou. Aby sme to nemuseli robiť zakaždým, keď sa vygeneruje schéma, moja utilitka to vie urobiť za nás. Schem*.XSD spravuje a súbory logicky premenuje týmto príkazom:

NillToMinOccurs [XSD] –sl

Odtiaľ sa dá stiahnúť demo (VS2010): http://accendo.sk/download/xsddemo.zip



[1] Klient sa v tomto kontexte myslí aplikácia, ktorá vola SOAP služby – samozrejme, že klientom v tomto prípade môže byť aj serverová aplikácia

[2] Jednalo sa o Britániu – neviem totiž či na SVK je podobný spôsob komunikácie so štátnym systémom niekde možný