Μια κατανοητή εισαγωγή στο SOAP, την XML, τα DTD και WSDL, με σύγκριση JSON και πρακτικό παράδειγμα C# σε Clean Architecture.

«Το API μας είναι SOAP. Θα σας στείλουμε το WSDL».

Κάπως έτσι μπορεί να ξεκινήσει μια διασύνδεση με έναν προμηθευτή. Η εφαρμογή σου έχει σύγχρονο frontend, το backend επιστρέφει JSON και ξαφνικά εμφανίζεται ένα XML γεμάτο Envelope, Body και ονόματα με άνω και κάτω τελεία.

Το SOAP υπάρχει για να βοηθά διαφορετικά συστήματα να συνεννοούνται. Για να το καταλάβεις, αρκεί να ξεχωρίσεις τρία πράγματα: πώς γράφονται τα δεδομένα, πώς οργανώνεται το μήνυμα και πώς συμφωνούν οι δύο πλευρές για το περιεχόμενό του.

Το SOAP είναι πρωτόκολλο ανταλλαγής μηνυμάτων, βασισμένο στην XML. Μια εφαρμογή στέλνει ένα δομημένο μήνυμα και η υπηρεσία το επεξεργάζεται σύμφωνα με συγκεκριμένους κανόνες. Έτσι, για παράδειγμα, ένα πρόγραμμα C# μπορεί να επικοινωνεί με μια υπηρεσία Java. Το SOAP υποστηρίζει διαφορετικά μοτίβα επικοινωνίας· εδώ θα ακολουθήσουμε το γνώριμο αίτημα–απάντηση. Εισαγωγή του W3C στο SOAP.

Η ιστορία ξεκινά το 1998. Οι πρώτες προσπάθειες συνδέθηκαν με τη Microsoft, τη DevelopMentor και τη UserLand. Στους πρωτεργάτες περιλαμβάνονται οι Don Box, Dave Winer, Bob Atkinson και Mohsen Al-Ghosein. Η αρχική ανάπτυξη του SOAP και του συγγενικού XML-RPC είχε κοινές ρίζες. Ιστορική αναδρομή του Don Box, αφήγηση του Dave Winer.

Η ανάγκη ήταν πρακτική: εφαρμογές διαφορετικών εταιρειών, γλωσσών και λειτουργικών συστημάτων έπρεπε να συνεργάζονται. Η XML πρόσφερε κοινό τρόπο περιγραφής πληροφοριών και το HTTP έναν ήδη διαδεδομένο δρόμο μεταφοράς τους. Το SOAP καθόρισε τις συμβάσεις επικοινωνίας πάνω σε αυτή τη βάση. Οι δημιουργοί εξηγούν τον αρχικό στόχο.

Ακολούθησαν περισσότερες εταιρείες, ανάμεσά τους η IBM. Στις 8 Μαΐου 2000 δημοσιεύτηκε το SOAP 1.1 ως W3C Note. Το SOAP 1.2 έγινε W3C Recommendation το 2003 και η δεύτερη έκδοσή του δημοσιεύτηκε το 2007. Γι’ αυτό ένα σημερινό project μπορεί να χρησιμοποιεί μια προδιαγραφή με ιστορία μεγαλύτερη των είκοσι ετών. SOAP 1.1, SOAP 1.2 του 2003, δεύτερη έκδοση SOAP 1.2.

Το όνομα προήλθε από το Simple Object Access Protocol, περίπου «Απλό Πρωτόκολλο Πρόσβασης σε Αντικείμενα». Τα αρχικά σχηματίζουν την αγγλική λέξη soap, δηλαδή «σαπούνι». Στο SOAP 1.2, το W3C διευκρινίζει ότι το SOAP χρησιμοποιείται πλέον ως όνομα και δεν αναπτύσσεται επίσημα σε ακρωνύμιο. Η διευκρίνιση στην προδιαγραφή.

Οι εταιρείες συνεχίζουν να το χρησιμοποιούν για συγκεκριμένους λόγους. Φαντάσου ένα ERP που επικοινωνεί με αποθήκες, λογιστήριο και δεκάδες συνεργάτες. Η αντικατάσταση της επικοινωνίας του απαιτεί αλλαγές, συντονισμό και ελέγχους σε όλες τις εμπλεκόμενες πλευρές.

Σε ένα τέτοιο περιβάλλον, η διατήρηση του SOAP μπορεί να εξηγείται από:

  • Υπάρχουσες διασυνδέσεις: λειτουργούν ήδη και εξυπηρετούν την επιχείρηση.
  • Απαιτήσεις συνεργατών: η υπηρεσία που πρέπει να καλέσεις διαθέτει SOAP interface.
  • Σαφή συμβόλαια: η περιγραφή της υπηρεσίας βοηθά στην παραγωγή client και στον έλεγχο συμβατότητας.
  • Υποδομές και τεχνογνωσία: η εταιρεία έχει επενδύσει σε εργαλεία, λειτουργία και συντήρηση.

Ένα συγκεκριμένο παράδειγμα είναι η Salesforce, η οποία τεκμηριώνει SOAP API και διαθέτει διαφορετικά WSDL για διαφορετικές ανάγκες διασύνδεσης. SOAP API της Salesforce.

Το ευρύτερο οικοσύστημα περιλαμβάνει και προδιαγραφές όπως το WS-Security, για tokens, ψηφιακές υπογραφές και κρυπτογράφηση τμημάτων του μηνύματος. Πρόκειται για πρόσθετους μηχανισμούς που χρειάζονται κατάλληλη υλοποίηση και ρύθμιση. Η χρήση SOAP από μόνη της δεν ενεργοποιεί αυτές τις δυνατότητες. Προδιαγραφή WS-Security.

Για να διαβάσουμε ένα μήνυμα, ας ζητήσουμε την τιμή ενός προϊόντος. Η υπηρεσία τιμών που ακολουθεί είναι υποθετική. Τα ονόματα, το namespace και το endpoint είναι αποκλειστικά για το παράδειγμα.

Ένα αίτημα SOAP 1.1 μπορεί να μοιάζει έτσι:

<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p="urn:example:prices"> <soap:Body> <p:GetPrice> <p:ProductCode>BK-42</p:ProductCode> </p:GetPrice> </soap:Body> </soap:Envelope> 

Σκέψου το σαν έναν φάκελο αλληλογραφίας:

Στοιχείο Ρόλος
Envelope Ο εξωτερικός φάκελος του μηνύματος.
Header Προαιρετικές πληροφορίες για την επεξεργασία του. Στο παράδειγμα παραλείπεται.
Body Το βασικό περιεχόμενο, εδώ το αίτημα GetPrice.
Fault Ειδικό στοιχείο μέσα στο Body για την περιγραφή SOAP σφάλματος.

Το SOAP ορίζει τον φάκελο και τους κανόνες επεξεργασίας. Η συγκεκριμένη υπηρεσία ορίζει τη λειτουργία GetPrice και το πεδίο ProductCode. Δομή μηνυμάτων SOAP.

Στην XML, το <p:ProductCode> ανοίγει ένα στοιχείο και το </p:ProductCode> το κλείνει. Ανάμεσά τους βρίσκεται η τιμή BK-42. Η δομή σχηματίζει δέντρο και τα στοιχεία πρέπει να εμφωλεύονται σωστά. Η XML ξεχωρίζει πεζά και κεφαλαία: ProductCode και productCode είναι διαφορετικά ονόματα. Τα attributes γράφονται μέσα στο αρχικό tag, όπως το encoding="utf-8". Ειδικοί χαρακτήρες χρειάζονται κατάλληλη γραφή, όπως &amp; για ένα & σε απλό κείμενο. Προδιαγραφή XML.

Τα soap: και p: είναι συντομογραφίες για namespaces, που αποφεύγουν τις συγκρούσεις ονομάτων. Η ταυτότητα ενός στοιχείου προκύπτει από το namespace και το τοπικό του όνομα. Μπορούμε να αλλάξουμε το prefix soap: σε s:, αρκεί να το συνδέσουμε με το ίδιο namespace. Αυτή η λεπτομέρεια έχει μεγάλη σημασία όταν γράφουμε parser. Namespaces στην XML.

Στη μεταφορά μέσω HTTP, το αίτημα SOAP 1.1 συνοδεύεται από headers όπως:

Content-Type: text/xml; charset=utf-8 SOAPAction: "urn:example:prices:GetPrice" 

Το endpoint και η ακριβής τιμή του SOAPAction προκύπτουν από το συμβόλαιο της υπηρεσίας. Το namespace λειτουργεί ως αναγνωριστικό και μπορεί να διαφέρει από τη διεύθυνση στην οποία στέλνουμε το αίτημα. SOAP 1.1 μέσω HTTP.

Η έκδοση χρειάζεται προσοχή: στο SOAP 1.2 το namespace του φακέλου είναι http://www.w3.org/2003/05/soap-envelope και ο τύπος περιεχομένου είναι application/soap+xml. Η πληροφορία action, όταν χρησιμοποιείται, μεταφέρεται ως παράμετρος αυτού του τύπου περιεχομένου. SOAP 1.2 και HTTP.

Η υπηρεσία μας απαντά:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p="urn:example:prices"> <s:Body> <p:GetPriceResponse> <p:Amount currency="EUR">19.90</p:Amount> </p:GetPriceResponse> </s:Body> </s:Envelope> 

Η διαδρομή είναι Envelope → Body → GetPriceResponse → Amount. Το περιεχόμενο του Amount δίνει την τιμή και το attribute currency το νόμισμα. Η απάντηση χρησιμοποιεί s: αντί για soap:, αλλά διατηρεί το ίδιο namespace.

Σε περίπτωση SOAP σφάλματος, το Body περιέχει Fault. Στο SOAP 1.1 ένα τέτοιο σφάλμα επεξεργασίας επιστρέφεται με HTTP 500 και SOAP Fault στο σώμα. Ο client χρειάζεται να εξετάζει και το περιεχόμενο της απάντησης για να αναγνωρίσει τι συνέβη. Σφάλματα SOAP 1.1.

DTD, XSD και WSDL περιγράφουν διαφορετικά πράγματα. Το DTD σημαίνει Document Type Definition και ορίζει κανόνες για τη δομή ενός XML εγγράφου: ποια στοιχεία επιτρέπονται, με ποια σειρά και ποια attributes είναι υποχρεωτικά. Ένα XML μπορεί να έχει σωστή σύνταξη, αλλά να παραβιάζει μια τέτοια περιγραφή. XML και DTD.

Εδώ υπάρχει μια λεπτομέρεια που εύκολα μπερδεύει: το SOAP απαγορεύει τη δήλωση DOCTYPE, μέσω της οποίας δηλώνεται DTD μέσα στο XML μήνυμα. Συνεπώς, η υποστήριξη DTD από την XML δεν σημαίνει ότι το χρησιμοποιούμε μέσα σε SOAP μηνύματα. Περιορισμοί SOAP.

Στα συμβόλαια SOAP θα συναντήσεις συνήθως XSD — XML Schema Definition. Το XSD περιγράφει δομή και τύπους: για παράδειγμα, ότι το Amount είναι xs:decimal ή ότι ένα πεδίο είναι προαιρετικό. Ο έλεγχος συμμόρφωσης με το schema απαιτεί την αντίστοιχη επικύρωση από την υλοποίηση. Εισαγωγή στο XML Schema.

Το WSDL — Web Services Description Language περιγράφει την ίδια την υπηρεσία: λειτουργίες, μηνύματα, bindings και endpoints. Τα XSD μπορούν να περιλαμβάνονται μέσα του ή να εισάγονται από άλλα αρχεία. Είναι το τεχνικό συμβόλαιο που διαβάζουν οι άνθρωποι και αξιοποιούν τα εργαλεία παραγωγής client. WSDL 1.1.

Η σύγκριση XML με JSON αφορά τη μορφή των δεδομένων. Η επιχειρηματική πληροφορία της απάντησής μας θα μπορούσε να γραφτεί σε JSON ως:

{ "amount": 19.90, "currency": "EUR" } 

Το JSON διαθέτει αντικείμενα, πίνακες, strings, αριθμούς, booleans και null. Παρά το όνομα JavaScript Object Notation, χρησιμοποιείται από πολλές γλώσσες. Προδιαγραφή JSON.

Χαρακτηριστικό XML JSON
Βασική γραφή Στοιχεία, tags και attributes Κλειδιά, τιμές και πίνακες
Πρόσθετη πληροφορία Attributes και namespaces Πεδία με συμφωνημένη σημασία
Περιγραφή τύπων Μέσω schema, όπως XSD Βασικοί τύποι στη σύνταξη και δυνατότητα πρόσθετου schema
Απλά μηνύματα δεδομένων Συνήθως περισσότερη σήμανση Συνήθως πιο συμπαγή
Κείμενο μαζί με δομημένα στοιχεία Υποστηρίζει mixed content Απαιτεί αντίστοιχη μοντελοποίηση

Το μικρότερο μέγεθος ενός μηνύματος δεν αρκεί για να προβλέψουμε την απόδοση μιας εφαρμογής. Παίζουν ρόλο η συμπίεση, ο parser, το δίκτυο και η επεξεργασία στον server.

Για να ξεχωρίζουμε τους όρους: SOAP είναι πρωτόκολλο, XML και JSON είναι μορφές δεδομένων, REST είναι αρχιτεκτονικό στυλ. Ένα REST API μπορεί να ανταλλάσσει JSON ή XML. Ο αρχικός ορισμός του REST από τον Roy Fielding.

Μέσα σε ένα project, ξεκινάμε από το συμβόλαιο. Αν βρεις αρχεία .wsdl και .xsd, μια χρήσιμη σειρά ανάγνωσης είναι:

  1. Εντοπίζεις τη λειτουργία που χρειάζεσαι, όπως GetPrice.
  2. Διαβάζεις τα μηνύματα εισόδου και εξόδου και τους τύπους τους.
  3. Ελέγχεις το binding για την έκδοση SOAP και τις λεπτομέρειες μεταφοράς.
  4. Βρίσκεις το endpoint και το απαιτούμενο action.
  5. Συγκρίνεις το συμβόλαιο με ένα παράδειγμα request και response.

Στο WSDL 1.1 θα συναντήσεις τα types, message, portType, binding, service και port, που οργανώνουν αυτές τις πληροφορίες. Δομή WSDL 1.1.

Σε .NET μπορείς να δημιουργήσεις client από το WSDL με το dotnet-svcutil ή το WCF Web Service Reference του Visual Studio. Ο παραγόμενος client προσφέρει μεθόδους C# και αναλαμβάνει την αντίστοιχη επικοινωνία. Οδηγός Microsoft για το dotnet-svcutil.

Η Clean Architecture κρατά την επιχειρηματική ανάγκη στον πυρήνα της εφαρμογής. Εδώ η ανάγκη είναι «βρες την τιμή του προϊόντος». Το SOAP είναι ο μηχανισμός επικοινωνίας με τον προμηθευτή. Το Application ορίζει ένα interface και το Infrastructure το υλοποιεί, ώστε οι εξαρτήσεις του κώδικα να δείχνουν προς τις εσωτερικές πολιτικές. Η Clean Architecture από τον Robert C. Martin.

Για το μικρό παράδειγμά μας, μια ενδεικτική διάταξη είναι:

MyShop.Application/ Prices.cs MyShop.Infrastructure/ Soap/ SoapPriceCatalog.cs MyShop.Api/ Program.cs 

Σε μεγαλύτερη εφαρμογή, οι επιχειρηματικές οντότητες και οι βασικοί κανόνες μπορούν να βρίσκονται στο Domain. Εδώ εστιάζουμε στη διασύνδεση. Η ουσία της αρχιτεκτονικής βρίσκεται στην κατεύθυνση των εξαρτήσεων.

Ο ακόλουθος κώδικας χρησιμοποιεί C# 12 και .NET 8 ή νεότερο, με ενεργά τα implicit usings των συνηθισμένων SDK templates. Στο Application/Prices.cs ορίζουμε το αποτέλεσμα, τη συμφωνία επικοινωνίας και την περίπτωση χρήσης:

namespace MyShop.Application; public sealed record ProductPrice(decimal Amount, string Currency); public interface IPriceCatalog { Task<ProductPrice> GetAsync( string productCode, CancellationToken cancellationToken = default); } public sealed class GetProductPrice(IPriceCatalog catalog) { public Task<ProductPrice> ExecuteAsync( string code, CancellationToken cancellationToken = default) => catalog.GetAsync(code, cancellationToken); } public sealed class PriceCatalogException(string message) : Exception(message) { } 

Το ProductPrice είναι το δικό μας αντικείμενο μεταφοράς δεδομένων, ένα DTO — Data Transfer Object. Το DTO είναι διαφορετική έννοια από το DTD της XML. Το IPriceCatalog εκφράζει αυτό που χρειάζεται η εφαρμογή, χωρίς τύπους κάποιου SOAP framework.

Στο Infrastructure/Soap/SoapPriceCatalog.cs υλοποιούμε την κλήση και την ανάγνωση της συγκεκριμένης απάντησης SOAP 1.1:

using System.Text; using System.Xml; using System.Xml.Linq; using MyShop.Application; namespace MyShop.Infrastructure.Soap; public sealed class SoapPriceCatalog(HttpClient http) : IPriceCatalog { private static readonly XNamespace Soap = "http://schemas.xmlsoap.org/soap/envelope/"; private static readonly XNamespace Prices = "urn:example:prices"; public async Task<ProductPrice> GetAsync( string productCode, CancellationToken cancellationToken = default) { var envelope = new XElement(Soap + "Envelope", new XElement(Soap + "Body", new XElement(Prices + "GetPrice", new XElement(Prices + "ProductCode", productCode)))); using var request = new HttpRequestMessage( HttpMethod.Post, "PriceService") { Content = new StringContent( envelope.ToString(), Encoding.UTF8, "text/xml") }; request.Headers.Add("SOAPAction", "\"urn:example:prices:GetPrice\""); using var response = await http.SendAsync( request, cancellationToken); var xml = await response.Content.ReadAsStringAsync( cancellationToken); var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }; using var input = new StringReader(xml); using var reader = XmlReader.Create(input, settings); var root = XDocument.Load(reader).Root; if (root?.Name != Soap + "Envelope") throw new PriceCatalogException("Μη αναμενόμενος φάκελος SOAP."); var body = root.Element(Soap + "Body") ?? throw new PriceCatalogException("Απουσιάζει το SOAP Body."); if (body.Element(Soap + "Fault") is not null) throw new PriceCatalogException("Ο πάροχος επέστρεψε SOAP Fault."); response.EnsureSuccessStatusCode(); var amount = body.Element(Prices + "GetPriceResponse") ?.Element(Prices + "Amount") ?? throw new PriceCatalogException("Απουσιάζει η τιμή."); var currency = amount.Attribute("currency")?.Value ?? throw new PriceCatalogException("Απουσιάζει το νόμισμα."); return new ProductPrice(XmlConvert.ToDecimal(amount.Value), currency); } } 

Η κατασκευή με XElement χειρίζεται τη διαφυγή ειδικών χαρακτήρων στα δεδομένα. Στην ανάγνωση, τα XNamespace μάς επιτρέπουν να εντοπίζουμε στοιχεία ανεξάρτητα από το prefix τους. Το DtdProcessing.Prohibit απορρίπτει DTD και το XmlConvert.ToDecimal μετατρέπει τη δεκαδική γραφή της XML σε decimal. XmlReaderSettings, XmlConvert.ToDecimal.

Ο έλεγχος για Fault προηγείται του EnsureSuccessStatusCode(), ώστε μια απάντηση SOAP Fault με HTTP 500 να αναγνωρίζεται ως τέτοια. Ο adapter επιστρέφει ένα ProductPrice με ποσό 19.90m και νόμισμα EUR.

Για να συνδέσουμε τα κομμάτια, στο API προσθέτουμε reference στο Application και στο Infrastructure. Το Infrastructure χρειάζεται reference στο Application. Το Application παραμένει ανεξάρτητο από το Infrastructure.

Ένα ελάχιστο Program.cs σε ASP.NET Core είναι:

using MyShop.Application; using MyShop.Infrastructure.Soap; var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient<IPriceCatalog, SoapPriceCatalog>(client => { client.BaseAddress = new Uri("https://supplier.example/"); client.Timeout = TimeSpan.FromSeconds(15); }); builder.Services.AddScoped<GetProductPrice>(); var app = builder.Build(); app.MapGet("/products/{code}/price", async ( string code, GetProductPrice getPrice, CancellationToken cancellationToken) => await getPrice.ExecuteAsync(code, cancellationToken)); app.Run(); 

Το supplier.example είναι υποθετική διεύθυνση. Για πραγματική υπηρεσία χρησιμοποιούμε το endpoint, τα namespaces, το action και τα σχήματα που ορίζει ο πάροχος.

Η διαδρομή της κλήσης είναι:

API → GetProductPrice → IPriceCatalog → SoapPriceCatalog → υπηρεσία SOAP 

Το δικό μας HTTP API μπορεί να επιστρέφει JSON στον καταναλωτή του, ενώ επικοινωνεί με SOAP στον προμηθευτή. Το Application διαχειρίζεται το ProductPrice που χρειάζεται. Αυτή η μετάφραση στο όριο της εφαρμογής μάς επιτρέπει να αλλάξουμε τον adapter όταν αλλάξει ο τρόπος επικοινωνίας του προμηθευτή, εφόσον διατηρούνται οι ίδιες επιχειρηματικές δυνατότητες.

Ο χειροκίνητος client παραπάνω δείχνει τη διαδρομή του μηνύματος. Σε ένα πραγματικό integration, το συμβόλαιο καθορίζει επιπλέον απαιτήσεις για ταυτοποίηση, headers και ασφάλεια. Ο client που παράγεται από WSDL μπορεί να αναλάβει μεγάλο μέρος της δουλειάς· τον τοποθετούμε πίσω από τον ίδιο adapter. Στο API προσθέτουμε την κατάλληλη αντιστοίχιση σφαλμάτων και κρατάμε τους αναλυτικούς κωδικούς Fault για διάγνωση.

Το SOAP συνεχίζει να δουλεύει εκεί όπου υπάρχουν συμφωνημένα συμβόλαια και συστήματα που βασίζονται σε αυτά. Πίσω από ένα απλό «δείξε μου την τιμή», διαφορετικές γενιές λογισμικού μπορούν να συνεργάζονται. Και με καθαρά όρια στην εφαρμογή, ο προγραμματιστής έχει μια συγκεκριμένη θέση για καθεμία από αυτές τις λεπτομέρειες.