SOAP XML WCF służy do  tworzenie usług sieciowych w .NET . ASP.NET MVC fantastycznie nadaje się do tworzenia aplikacji sieciowych REST. Co znaczy, że WCF w  wypadku usług typu REST nie jest  już tak bardzo potrzebny. Ma on jednak wciąż swoje miejsce w rodzinie .NET jako framework do tworzenia usług typu SOAP.

 

Protokół SOAP został stworzony przez Microsoft i mimo swojej nazwy nie jest jednak taki prosty jak mydło.  SOAP wysyła wiadomości wyłącznie w formacie XML. Microsoft stworzył SOAP z myślą o zastąpieniu jeszcze starszych technologii jak DCOM,  czy protokołów binarnych przekazu informacji, jak np. CORBA. (Common Object Request Broker Architecture).  

SOAP dostał swój standard i śmiało można z niego korzystać nawet dziś. Główną jego zaletą jest  większe bezpieczeństwo niż w usługach typu REST. Posiada też wbudowaną obsługę błędów. 

SOAP w swoich komunikatach zwrotnych, jak i zapytaniach wymaga dużej ilości informacji w formacie XML. Komunikaty więc są dużo pojemniejsze niż w usługach REST. Co więcej, przez to skomplikowanie powstała idea, aby na podstawie pliku WSDL tworzyć obiekty klas w Javie i w C#, które później za nas zostały przetłumaczone na odpowiedzi lub zapytania XML do usługi SOAP.

Taki styl programowania jest preferowany. Pisanie swojego kodu wysyłającego XML do usługi wydaje się dodatkową pracą, ale czy zawsze tak jest?

Ostatnio zderzyłem się z takim  scenariuszem, gdzie nagle okazało się, że napisanie własnego kodu, który by generował taki XML byłoby zapewne lepszym rozwiązaniem.

Problem powstał przez pewną usługę SOAP stworzoną przez pewną polską instytucję.

Ta usługa SOAP ma polskie nazwy, dziwne typy wyliczeniowe np. “TAK”, “NIE” i ogólnie nie jest dobrym przykładem, jak tworzyć przyjazną usługę, z której wszyscy będą korzystać.

Klasy  utworzone przez plik WSDL byłyby więc koszmarne. Czasem pola są typu Object, ponieważ można w nich umieszczać wiele niepowiązanych ze sobą typów.

Klasy te nie są sympatyczne, więc w tym projekcie istnieją inne klasy, które lepiej wyrażają kwestie biznesowe tej aplikacji. Te lepsze klasy są potem tłumaczone na klasy WSDL po to, aby potem je wysłać przez SOAP w formacie XML.

W takiej sytuacji, przy tak słabym kodzie klas WSDL  i translatorów obiektów - zadałem sobie bardzo ważne pytanie.

Czy można było to zrobić  lepiej? Np. napisać kod tłumaczący moje klasy na XML bez patrzenia na te dziwne klasy WSLD. 

W takich wypadkach wydaje się to słusznym rozwiązaniem. 

WCF SOAP i jego klasy proxy na podstawie WSDL

Na potrzeby tego przykładu oczywiście potrzebuję usługi SOAP. Wiem, że mojego bloga czyta dużo osób, które zaczynają swoją przygodę z programowaniem, dlatego postanowiłem pokazać wszystko krok po kroku.

Uruchomiłem więc Visual Studio 2017 i stworzyłem aplikację WCF. 

wcf Service Appliccation

By ukazać jak serializacja XML wygląda postanowiłem utworzyć parę złożonych obiektów.

CompositeTypes

Zapytanie do naszej usługi będzie jednak proste. Będę wyszukiwał historie pojazdów na podstawie ich numeru VIN oraz numeru rejestracyjnego. 

Do klas wykorzystywanych przez WCF trzeba dodać odpowiednie atrybuty. Tak aby WCF wiedział jakie właściwości ma serializować. Do atrybutu DataContract warto też dodać swoją przestrzeń nazw. Normalnie w klasach proxy jej nie widać, ale gdy będziemy wysłać czyste XML te przestrzenie nazw będą bardziej przejrzyste niż nasze programistyczne przestrzenie nazw w C#.

[DataContract(Namespace = "http://cezary.pl/")]
public class RequestByVehicle
{
    [DataMember]
    public string VIN { get; set; }

    [DataMember]
    public string RegistrationNumber { get; set; }
}

W odpowiedzi będę potrzebował bardziej złożone klasy. Oto klasa właściciela pojazdu.

[DataContract(Namespace = "http://cezary.pl/")]
public class Owner
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public string LastName { get; set; }
}

Oto klasa pojazdu, która zawiera listę właścicieli.

[DataContract(Namespace = "http://cezary.pl/")]
public class Vehicle
{
    [DataMember]
    public IList<Owner> Owners { get; set; }
    [DataMember]
    public string Model { get; set; }
    [DataMember]
    public string VIN { get; set; }
    [DataMember]
    public string RegistrationNumber { get; set; }
}

Oto typ wyliczeniowy określający stan odpowiedzi. Warto tu zaznaczyć, że serializacją typu wyliczeniowego jest bardziej kłopotliwa i wymaga atrybutu EnumMember do każdej swojej wartości. 

[DataContract(Namespace = "http://cezary.pl/")]
public enum ResponseState
{
    [EnumMember]
    OK,
    [EnumMember]
    NotFound,
    [EnumMember]
    Error
}

Mają to wszystko możemy w końcu utworzyć klasę odpowiedzi.

[DataContract(Namespace = "http://cezary.pl/")]
public class ResponseVehicleHistory
{
    [DataMember]
    public IList<Vehicle> Vehicles { get; set; }

    [DataMember]
    public ResponseState State { get; set; }
}

Czas zmodyfikować interfejs naszej usługi WCF i dodać do niej naszą metodę szukania historii pojazdu. Ważnym parametrem jest tutaj parametr ACTION, który będzie bardzo ważny przy wywołaniu XML tej metody.  Dla tej metody ma ona wartość Vehicle.

[ServiceContract(Namespace = "http://cezary.pl/")]
public interface IServiceExample
{

    [OperationContract(Action = "Vehicle")]
    ResponseVehicleHistory GetVehicleHistory(RequestByVehicle request);


}

Klasa usługi dla tego przykładu będzie zwracać obiekt odpowiedzi tylko dla numeru VIN 1FUBCYDA96HV48955.

[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
public class ExampleService : IServiceExample
{
    public ResponseVehicleHistory GetVehicleHistory(RequestByVehicle request)
    {
        if (request.VIN == "1FUBCYDA96HV48955")
        {
            return new ResponseVehicleHistory()
            {
                State = ResponseState.OK,
                Vehicles = new List<Vehicle>()
                {
                    new Vehicle()
                    {
                        Model = "Fiat",
                        VIN = "1FUBCYDA96HV48955",
                        Owners = new List<Owner>()
                        {
                            new Owner()
                            {
                                LastName = "Walenciuk", Name = "Cezary"
                            }
                        },
                        RegistrationNumber = "N4699"
                    }
                }
            };
        }

        return new ResponseVehicleHistory() { State = ResponseState.NotFound };
    }
}

Nie zapomnij także o dokumencie markup. Zmieniliśmy nazwy interfejsu i klasy usługi WCF. Zmiany także trzeba wprowadzić w tym dokumencie - markup. 

View Markup

<%@ ServiceHost Language="C#" Debug="true" Service="CallingWCFExample.ExampleService" 
CodeBehind="ExampleService.svc.cs" %>

Usługę można przetestować w WCF Test Client, ale nie będzie nam to potrzebne, ponieważ szybko stworzymy aplikację konsolową, która przy pomocy proxy wywoła tę usługę.

WCF Test Client

Tworzy więc aplikację konsolową.

Console App

Do niej dodajemy referencję do naszej usługi SOAP. Na podstawie tej referencji zostaną utworzone klasy Proxy.

Add Service Reference

Następnie ukazuje się nam takie okno. W nim klikamy na guzik “Discover”. Domyślnie nasza aplikacja WCF korzysta z serwera IIS Express. Dlatego mamy adres localhost z 4 cyfrowym portem. Możemy to zmienić.

Add Service Reference

Wracając do projektu WCF w zakładce WEB możemy ustawić inne proxy lub…

WEB WCF IIS Express

Zmienić nasz serwer IIS Express na lokalny serwer IIS.

Local IIS

By wprowadzić tą zmianę musisz mieć uruchomione Visual Studio z uprawnieniami administratora.

0007

Wracająca do okna “Add Service Reference” daj jakąś sensowną przestrzeń nazw i kliknij ok.

Local IIS ADD Service Reference

W naszej aplikacji konsolowej w pliku konfiguracyjnym zostały dodane informacje o połączeniu do usługi WCF. 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IServiceExample" />
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost/CallingWCFExample/ExampleService.svc"
                binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IServiceExample"
                contract="ServiceExample.IServiceExample" name="BasicHttpBinding_IServiceExample" />
        </client>
    </system.serviceModel>
</configuration>

Wywołanie  usługi WCF przy pomocy proxy jest bardzo proste. Klasa <nazwa_usług>Client ma wszystkie metody naszej usługi WCF. Operujemy na tych samych obiektach co w projekcie WCF. Pamiętajmy jednak, że zostaną one później pomiędzy klientem a serwerem serializowane do formatu XML.

class Program
{
    static void Main(string[] args)
    {
        ServiceExampleClient client = new ServiceExampleClient();

        var response = client.GetVehicleHistory(new RequestByVehicle() { VIN = "1FUBCYDA96HV48955" });

        foreach (var item in response.Vehicles)
        {
            Console.WriteLine(item.Model);
            Console.WriteLine(item.RegistrationNumber);
            Console.WriteLine(item.VIN);

            foreach (var owner in item.Owners)
            {
                Console.WriteLine("\t" +
                    owner.Name + " " + owner.LastName);
            }
        }

        Console.ReadKey();
    }
}

Teraz gdy mamy usługę WCF i prostą aplikację, która wywołuje naszą aplikację przy pomocy klas proxy – zobaczmy jak wywołać naszą usługę WCF czystym XML.

Wysyłamy czysty XML do SOAP przy użyciu SOAPUI

Rodzi się podstawowe pytanie. Jak ten XML ma wyglądać? W teorii na bazie pliku WSDL i znajomości dokumentacji SOAP dałoby radę odręcznie taki XML stworzyć.

Po co jednak utrudniać sobie życie. Zwłaszcza gdy mamy takie narzędzia jak SOAPUI, które taki XML nam utworzą na podstawie WSDL . Jeśli więc nie masz narzędzia SOAPUI, to na potrzeby tego ćwiczenia koniecznie go zainstaluj. 

Stwórz więc nowy projekt SOAP w tym programie. 

New SOAP Project

Adres do dokumentu WSLD znajduje się w adresie SVC naszej usługi.

ExampleService WSLD

W oknie projektu SOAP wpis adres do dokumentu WSDL.

New SOAP Project

SOAP UI zlokalizował metody naszej usługi WCF i utworzył domyślne zapytanie do naszej metody.  

SOAP Project

Klikając dwa razy na “Request 1” ukaże się okno a w nim właśnie ten kod XML, który jest nam potrzebny do wywołania naszej usługi SOAP.

Request Soap UI

Pytanie XML do naszej metody WCF wygląda więc tak. Zwróć uwagę na fakt, że wszystkie parametry są opcjonalne. Wynika to z tego, że typ string nie musi mieć wartości. Co jest interpretowane jako brak wymogu jego egzystencji w składni XML.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cez="http://cezary.pl/">
   <soapenv:Header/>
   <soapenv:Body>
      <cez:GetVehicleHistory>
         <!--Optional:-->
         <cez:request>
            <!--Optional:-->
            <cez:RegistrationNumber>?</cez:RegistrationNumber>
            <!--Optional:-->
            <cez:VIN>1FUBCYDA96HV48955</cez:VIN>
         </cez:request>
      </cez:GetVehicleHistory>
   </soapenv:Body>
</soapenv:Envelope>

WCF w swojej odpowiedzi też zwraca XML.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <GetVehicleHistoryResponse xmlns="http://cezary.pl/">
         <GetVehicleHistoryResult xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
            <State>OK</State>
            <Vehicles>
               <Vehicle>
                  <Model>Fiat</Model>
                  <Owners>
                     <Owner>
                        <LastName>Walenciuk</LastName>
                        <Name>Cezary</Name>
                     </Owner>
                  </Owners>
                  <RegistrationNumber>N4699</RegistrationNumber>
                  <VIN>1FUBCYDA96HV48955</VIN>
               </Vehicle>
            </Vehicles>
         </GetVehicleHistoryResult>
      </GetVehicleHistoryResponse>
   </s:Body>
</s:Envelope>

Teraz jak to wszystko zrobić w kodzie w C#.

Wysyłamy czysty XML do SOAP przy użyciu kodu

W kolejnej aplikacji konsolowej nie będziemy potrzebować klas proxy. Wywołanie usługi SOAP w kodzie przy pomocy czystego XML-a można wykonać na dwa sposoby.

Console

Pierwszy sposób polega na użyciu klasy WebClient, która nie wymaga dużej ilości parametrów. W nagłówku zapytania HTTP muszę podać jaki typ będzie zawierać moje zapytanie. W tym wypadku content-type to text/xml.

Muszę też podać nagłówek SOAPAction. W najnowszej wersji SOAP nie jest to wymagane, ale WCF widocznie nie wspiera tej najnowszej wersji SOAP. 

W nagłówku SOAPAction wpisuję nazwę akcji mojej metody WCF. Jak dobrze pamiętamy nazwaliśmy ją “Vehicle”. 

public class WebClientSoapCaller
{
    public static string SendSoap(string xmlRawData)
    {
        var client = new WebClient();
        try
        {
            var data = xmlRawData;
            // the Content-Type needs to be set to XML
            client.Headers.Add("Content-Type", "text/xml;charset=utf-8");
            // The SOAPAction header indicates which method you would like to invoke
            // and could be seen in the WSDL: <soap:operation soapAction="..." /> element
            client.Headers.Add("SOAPAction", "Vehicle");

            var response = client.UploadString
		("http://localhost/CallingWCFExample/ExampleService.svc", data);
            Console.WriteLine(response);
        }
        catch (Exception e)
        {
            if (e is WebException && ((WebException)e).Status == WebExceptionStatus.ProtocolError)
            {
                WebResponse errResp = ((WebException)e).Response;
                using (Stream respStream = errResp.GetResponseStream())
                {
                    StreamReader reader = new StreamReader(respStream);
                    string text = reader.ReadToEnd();
                    return text;
                }
            }
        }
        finally
        {
            client.Dispose();
        }
        return "";
    }
}

Jak widać w przechwyceniu wyjątku znajduje się dużo kodu. Ten kod przechwyci mi więcej informacji o błędzie, gdy moja usługa WCF na poziomie serwera, bądź serializacji wyrzuci  błąd serwerowy 500 lub inny.

Swoją usługę SOAP mogę też wywołać przy pomocy klasy HTTPWebRequest. Wymaga to jednak większej ilości kodu. 

public class SoapHttpWebRequestCaller
{
    public static string SendSoap(string xmlRawData)
    {
        string url = "http://localhost/CallingWCFExample/ExampleService.svc";

        XmlDocument soapEnvelopeXml = new XmlDocument();
        soapEnvelopeXml.LoadXml(xmlRawData);

        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
        webRequest.ContentType = "text/xml;charset=\"utf-8\"";
        webRequest.Accept = "text/xml";
        webRequest.Method = "POST";
        webRequest.Headers.Add("SOAPAction", "Vehicle");

        using (Stream stream = webRequest.GetRequestStream())
        {
            soapEnvelopeXml.Save(stream);
        }

        IAsyncResult asyncResult = webRequest.BeginGetResponse(null, null);
        asyncResult.AsyncWaitHandle.WaitOne();
        string soapResult;
        WebResponse webResponse = null;

        try
        {
            webResponse = webRequest.EndGetResponse(asyncResult);

            using (StreamReader rd = new StreamReader(webResponse.GetResponseStream()))
            {
                soapResult = rd.ReadToEnd();
            }

            return soapResult;
        }
        catch (Exception e)
        {
            if (e is WebException && ((WebException)e).Status 
		== WebExceptionStatus.ProtocolError)
            {
                WebResponse errResp = ((WebException)e).Response;
                using (Stream respStream = errResp.GetResponseStream())
                {
                    StreamReader reader = new StreamReader(respStream);
                    string text = reader.ReadToEnd();
                    return text;
                }
            }
        }
        finally
        {
            webResponse.Close();
        }
        return "";
    }
}

Sprawdź jedną z tych metod podając czysty XML.

class Program
{
    static void Main(string[] args)
    {

        string request = @"<soapenv:Envelope xmlns:soapenv=""http://schemas.xmlsoap.org/soap/envelope/"" 
		xmlns:cez=""http://cezary.pl/"">
           <soapenv:Header/>
           <soapenv:Body>
              <cez:GetVehicleHistory>
                 <cez:request>
                    <cez:RegistrationNumber></cez:RegistrationNumber>
                    <cez:VIN>1FUBCYDA96HV48955</cez:VIN>
                 </cez:request>
              </cez:GetVehicleHistory>
           </soapenv:Body>
        </soapenv:Envelope>";



        var stringa = SoapHttpWebRequestCaller.SendSoap(request);
        Console.WriteLine(PrettyXml(stringa));
        Console.ReadKey();
    }

    static string PrettyXml(string xml)
    {
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(xml);

        StringBuilder sb = new StringBuilder();
        XmlWriterSettings settings = new XmlWriterSettings
        {
            Indent = true,
            IndentChars = "  ",
            NewLineChars = "\r\n",
            NewLineHandling = NewLineHandling.Replace
        };
        using (XmlWriter writer = XmlWriter.Create(sb, settings))
        {
            doc.Save(writer);
        }
        return sb.ToString();
    }
}

Jak widać wywołanie usługi SOAP samym XML jest dosyć proste. Pamiętać jednak trzeba, że ten XML do zapytania musi być przez Ciebie jakoś utworzony. 

Możesz to zrobić ręcznie pisząc swoje klasy. W większości wypadków nie jest to najbardziej wydajne rozwiązanie. Jednak, jeśli pracujesz z bardzo złośliwą usługą SOAP, gdzie klasy proxy są dziwne, to warto przemyśleć takie rozwiązanie. Twój kod, twój wybór - dokument WSDL czasami jest obiektowym śmietnikiem.