70-503: Defining Behavioral Contracts

Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.

Począwszy od .NET Framework w wersji 3.0 WCF, czyli Windows Communication Foundation, jest jego częścią. Możemy korzystać z niego w celu budowania aplikacji, których architektura składa się z wielu połączonych ze sobą serwisów, które wzajemnie się komunikują. Zaczynając przygodę z WCF zaczniemy od wyjaśnienia kontraktów opisujących zachowanie (ang. behaviour) serwisów, czyli opisu tego z czym możemy się komunikować, oraz w jaki sposób (poprzez jakie operacje).

Szybki start

Zaczniemy od utworzenia projektu typu “WCF Service Library”. Nazwijmy go Calculator.Services.

image

W ramach projektu utworzony zostanie przykładowy serwis – IService1. Warto zapoznać się z wygenerowanym kodem, który wtajemnicza nas w podstawy tworzenia usług sieciowych. Chcemy jednak szybko zobaczyć pierwsze efekty, więc uruchamiamy usługę – F5 (Debug > Start Debugging). Wraz z usługą uruchomiona zostanie aplikacja WCF Test Client, która pomoże nam podczas testów. Na poniższej ilustracji zostały ukazane parametry wejściowe (niebieska strzałka) oraz wyjściowe (zielona strzałka) metody GetDataUsingDataContract.

image

Omówienie kodu

Pierwszą sprawą, na która należy zwrócić uwagę jest automatycznie dodana referencja do komponentu System.ServiceModel. Gdybyśmy nie korzystali z przykładowego szkieletu projektu musielibyśmy dodać go ręcznie. Dodatkowo w obu plikach źródłowych (IService1.cs, Service1.cs) dodana jest dyrektywa using wczytująca wspomnianą przestrzeń nazw:

   1: using System.ServiceModel;

Kontrakt serwisu oznacza mechanizm, dzięki któremu możliwości i wymagania zostają zdefiniowane. Dzięki tej definicji klient serwisu wie jak się z nim porozumiewać. W .NET Framework składa się na niego:

  • interfejs lub klasa (zaleca się jednak korzystanie wyłącznie z interfejsów!),
  • operacje wewnątrz interfejsu (klasy),
  • trzy specjalne atrybuty: ServiceContractAttribute, OperationContractAttribute, oraz MessageParameterAttribute.

Plik IService1.cs zawiera definicję interfejsu/kontraktu IService1:

   1: [ServiceContract]
   2: public interface IService1
   3: {
   4:     [OperationContract]
   5:     string GetData(int value);
   6:     // ...
   7: }

Widzimy tu, że zostały wykorzystane dwa spośród wymienionych trzech atrybutów. Jest to domyślna forma pozbawiona parametrów, które pozwalają wprowadzić dodatkowe informacje do opisu.

Plik Service1.cs stanowi implementację interfejsu. Powody wykorzystania DataContractAttribute zostaną wyjaśnione w następnym artykule.

Plik WSDL naszego serwisu dostępny będzie pod adresem http://localhost:8732/Design_Time_Addresses/Calculator.Services/Service1/?wsdl.

Kalkulator – trochę trudniejszy przykład

Kolejnym przykładem będzie kalkulator, który potrafi dodawać, mnożyć, oraz dodawać wartości na stos, oraz je z niego pobierać. Cel napisania serwisu to pokazanie bardziej zaawansowanych opcji. Zmieniamy nazwy wygenerowanych plików, odpowiednio IService1.cs na ICalculator.cs, Service1.cs na Calculator.cs. Nowa zawartość pliku ICalculator.cs będzie wyglądała następująco:

   1: using System.ServiceModel;
   2:  
   3: namespace Calculator.Services
   4: {
   5:     [ServiceContract(Name = "Calculator",
   6:         Namespace = "http://schemas.eastgroup.pl/2010/02/26/calculator/")]
   7:     public interface ICalculator
   8:     {
   9:         [OperationContract(IsOneWay = true)]
  10:         void Push([MessageParameter(Name = "double")]double value);
  11:  
  12:         [OperationContract]
  13:         [return: MessageParameter(Name = "value")]
  14:         double Pop();
  15:  
  16:         [OperationContract]
  17:         double Add(double a, double b);
  18:  
  19:         [OperationContract]
  20:         double Multiply(double a, double b);
  21:     }
  22: }

Jak już wcześniej pisaliśmy atrybuty kontraktu mogą przyjmować parametry. Na powyższym przykładzie ServiceContractAttribute korzysta z dwóch – Name i Namespace. Nazwę serwisu zmieniamy aby pozbyć się prefiksu “I”, ponieważ poprzedzanie interfejsów tą literą jest bardzo często konwencją, przestrzeń nazw określamy, aby zapewnić naszemu serwisowi pewną unikalność. Domyślna przestrzeń nazw to http://tempuri.org. Pozostałe możliwe parametry to: CallbackContact, ProtectionLevel, ConfigurationName, SessionMode.

W OperationContractAttribute metody Push dodany został parametr IsOneWay, który mówi, że wywołując metodę nie otrzymamy żadnego komunikatu zwrotnego – nasz komunikat jest jednokierunkowy.Inne możliwe parametry to:

  • Name – zmieniamy nazwę domyślną operacji na inną,
  • Action – zmieniamy nagłówek operacji w zapytaniu do serwisu (domyślny może wygladać np. tak - http://schemas.eastgroup.pl/2010/02/26/calculator/Calculator/Add),
  • ReplyAction – nagłówek akcji z odpowiedzi (np. http://schemas.eastgroup.pl/2010/02/26/calculator/Calculator/AddResponse),
  • ProtectionLevel – informacja w jaki sposób komunikaty powinny być chronione (podpisane, szyfrowane),
  • IsInitiating – zarządzanie sesją; określa, czy wywołanie operacji rozpoczyna sesję pomiędzy wywołującym a serwisem,
  • IsTerminating – kończenie sesji.

Ciekawą sprawą jest to, że parametr Action może przyjmować jako parametr m.in. gwiazdkę. W ten sposób możemy utworzyć operację (metodę), która będzie wywoływana, jeżeli komunikat nie będzie pasował do żadnej innej operacji:

   1: [OperationContract(IsOneWay=true, Action="*")]
   2: void ProcessUnrecognizedMessage(Message message);

W takim wypadku należy dodać także dyrektywę using:

   1: using System.ServiceModel.Channels;

MessageParameterAttribute może przyjąć tylko jeden parametr, którym jest Name. Atrybut ten służy do kontrolowania nazw parametrów operacji, oraz nazw wartości zwracanych. W naszym przykładzie parametr operacji Push będzie nazywał się double, a nie value, wynik wywołania operacji Pop zostanie zwrócony jako value, a nie PopResult.

Implementacja interfejsu – Calculator.cs:

   1: using System.Collections.Generic;
   2: using System.ServiceModel;
   3:  
   4: namespace Calculator.Services
   5: {
   6:     class Calculator : ICalculator
   7:     {
   8:         private Stack<double> stack;
   9:  
  10:         public Calculator()
  11:         {
  12:             stack = new Stack<double>();
  13:         }
  14:  
  15:         #region ICalculator Members
  16:  
  17:         public void Push(double value)
  18:         {
  19:             stack.Push(value);
  20:         }
  21:  
  22:         public double Pop()
  23:         {
  24:             return stack.Pop();
  25:         }
  26:  
  27:         public double Add(double a, double b)
  28:         {
  29:             return a + b;
  30:         }
  31:  
  32:         public double Multiply(double a, double b)
  33:         {
  34:             return a * b;
  35:         }
  36:  
  37:         #endregion
  38:     }
  39: }

Po złożeniu projektu warto odpalić usługę i potestować ją trochę.
Uwaga: W programie WCF Test Client po wywołaniu operacji warto zajrzeć do zakładki XML – jeżeli określiliśmy np. parametr Action w OperationContractAttribute zobaczymy go właśnie tu.

W poszukiwaniu błędów

Pomimo, że przykład kalkulatora jest dość prosty nie ustrzegł się błędów – zdejmowanie elementów z pustego stosu spowoduje wyrzucenie wyjątku InvalidOperationException. WCF pozwala nam określić kiedy serwis może zwrócić informację o błędzie (ang. fault), oraz jaką informację powinien przy okazji przekazać.

Fault i Exception (wyjątek) to dwa mechanizmy. Fault dotyczy protokołu SOAP, Exception to mechanizm frameworka .NET. WCF dostarcza nam wyjątek o nazwie FaultException, który jest połączeniem obu tych dziedzin – rzucony wyjątek zostanie poddany serializacji i przekazany jako fault do klienta usługi. Dodatkowo atrybut FaultContractAttribute pozwala określić, które operacje zwracają błędy, i jeśli zwracają, to jakie.

Aktualizacja metody Pop w interfejsie, oraz dodanie metody Divide:

   1: [OperationContract]
   2: // błąd będzie typu string
   3: [FaultContract(typeof(string))]
   4: [return: MessageParameter(Name = "value")]
   5: double Pop();
   6:  
   7: [OperationContract]
   8: [FaultContract(typeof(string))]
   9: double Divide(double a, double b);

Implementacja:

   1: public double Pop()
   2: {
   3:     if (stack.Count == 0)
   4:     {
   5:         string faultDetail = "Stack is empty!";                
   6:         throw new FaultException<FaultInfo>(fi, new FaultReason(faultDetail));
   7:     }
   8:     return stack.Pop();
   9: }
  10:  
  11: public double Divide(double a, double b)
  12: {
  13:     if (b == 0.00d)
  14:     {
  15:         string faultDetail = "You cannot divide by zero";
  16:         throw new FaultException<string>(faultDetail, new FaultReason(faultDetail));
  17:     }
  18:  
  19:     return a / b;
  20: }

Informacja przekazana nie musi być typu string. Gdybyśmy chcieli przekazać własny obiekt, musimy go najpierw utworzyć – plik FaultInfo.cs:

   1: using System.Runtime.Serialization;
   2:  
   3: namespace Calculator.Services
   4: {
   5:     [DataContract(Namespace="http://schemas.eastgroup.pl/2010/02/26/calculator/")]
   6:     class FaultInfo
   7:     {
   8:         [DataMember]
   9:         public string Reason = null;
  10:     }
  11: }

Następnie należy zaktualizować aktualny kod. Plik ICalculator.cs:

   1: [OperationContract]
   2: [FaultContract(typeof(FaultInfo))]
   3: [return: MessageParameter(Name = "value")]
   4: double Pop();
   5:  
   6: [OperationContract]
   7: [FaultContract(typeof(FaultInfo))]
   8: double Divide(double a, double b);

Plik Calculator.cs:

   1: public double Pop()
   2: {
   3:     if (stack.Count == 0)
   4:     {
   5:         string faultDetail = "Stack is empty!";
   6:         FaultInfo fi = new FaultInfo()
   7:         {
   8:             Reason = faultDetail
   9:         };
  10:         throw new FaultException<FaultInfo>(fi, new FaultReason(faultDetail));
  11:     }
  12:     return stack.Pop();
  13: }
  14:  
  15: public double Divide(double a, double b)
  16: {
  17:     if (b == 0.00d)
  18:     {
  19:         string faultDetail = "You cannot divide by zero";
  20:         FaultInfo fi = new FaultInfo()
  21:         {
  22:             Reason = faultDetail
  23:         };
  24:         throw new FaultException<FaultInfo>(fi, new FaultReason(faultDetail));
  25:     }
  26:  
  27:     return a / b;
  28: }

Uwaga: Jeżeli nie dodamy tworzenia obiektu FaultReason podczas błędy otrzymamy komunikat:

System.ServiceModel.FaultException`1 was unhandled by user code
  Message=The creator of this fault did not specify a Reason.

Kiedy kiedy teraz uruchomimy serwis i wywołamy błąd otrzymujemy komunikat “System.ServiceModel.FaultException`1 was unhandled by user code”:

image W podlinkowanym wpisie jako powód tego problemu wymienione są ustawienie debuggera Visual Studio. Aby wyłączyć komunikat należy przejść w menu Tools > Options… > w zakładce Debugging wyłączyć opcje Enable the exception assistant, oraz Enable Just My Code (Managed only).

image 

Teraz przy zdejmowaniu elementów z pustego stosu dostaniemy komunikat błędu:

image

Dokładnie to, o co nam chodziło! Kiedy zaczniemy już sami pisać aplikację klienta zrobimy ładniejszą obsługę wyjątku.

Message Exchange Patterns

MEP określa, z jakiego typu komunikacji korzysta operacja. Możliwe rozwiązania, to:

  • żądanie/odpowiedź (request/response),
  • komunikacja jednokierunkowa - OneWay,
  • komunikacja dwukierunkowa - Duplex.

Pierwszy typ komunikacji jest domyślny dla wszystkich operacji. Komunikacja staje się jednokierunkowa, jeśli ustawimy parametr IsOneWay na true. Wysyłanie komunikatów w jedną stronę wiąże się z pewnymi niedogodnościami:

  • nie można jej łączyć z FaultContractAttribute – nie ma możliwości odesłania informacji o błędzie,
  • nie mamy pewności, że operacja zakończyła się sukcesem,

Komunikacja dwukierunkowa ma zastosowanie, gdy:

  • klient wysyła komunikat do serwisu inicjując tym długotrwały proces, następnie oczekuje na informację o zakończeniu przetwarzania,
  • klient musi być w stanie otrzymywać komunikaty z serwisu, które nie są odpowiedziami na żądanie.

Dwukierunkowy MEP deklaruje się poprzez powiązanie kontraktu serwisu z kontraktem typu Callback. Powiązanie to wykonuje się poprzez właściwość CallbackContract atrubutu ServiceContractAttribute. Jako przykład może posłużyć prosta aplikacja, do której wysyłamy jednokierunkowy komunikat, a po pewnym czasie, po przetworzeniu komunikatu, serwis wysyła komunikat do klienta i informuje go o zakończeniu działania. Poniższy kod pokazuje kontrakty, oraz sposób ich powiązania:

   1: [ServiceContract]
   2:  interface IGreetingHandler
   3:  {
   4:      [OperationContract(IsOneWay = true)]
   5:      void GreetingProduced(string greeting);
   6:  }
   7:  
   8:  [ServiceContract(CallbackContract = typeof(IGreetingHandler))]
   9:  interface IGreetingService
  10:  {
  11:      [OperationContract(IsOneWay = true)]
  12:      void RequestGreeting(string name);
  13:  }
  14:  
  15:  [ServiceBehavior(InstanceContextMode =
  16:  InstanceContextMode.PerSession)]
  17:  class GreetingService : IGreetingService
  18:  {
  19:      public void RequestGreeting(string name)
  20:      {
  21:          Console.WriteLine("In Service.Greet");
  22:          IGreetingHandler callbackHandler =
  23:            OperationContext.Current.GetCallbackChannel<IGreetingHandler>();
  24:          callbackHandler.GreetingProduced("Hello " + name);
  25:      }
  26:  }

Pokazany został tu także sposób w jaki implementacja serwisu korzysta z OperationContext, aby otrzymać referencję do kontraktu typu Callback, przez co serwis może odpowiedzieć klientowi. Kod ten nie jest jednak pełną solucją. Kilka dodatkowym kroków musi zostać wykonane po stronie klienta, aby komunikacja dwukierunkowa zadziałała. Sprawa zostanie dokładniej omówiona w jednym z następnych artykułów.

Chociaż taki sposób komunikacji wydaje sie bardzo ciekawy, okazuje się, że nie zawsze się sprawdza. Przede wszystkim często nie jest możliwy do osiągnięcia z powodu firewalli, czy usług NAT. Ogranicza go to do zastosowania wewnątrz organizacji. Dodatkowo wymuszą utrzymywanie sesji pomiędzy klientem a serwisem i bardzo długie jej podtrzymywanie. Kolejny minus to fakt, że jest to mechanizm specyficzny dla WCF i nie ma szans na korzystanie z niego z poziomu np. Javy.

W następnej lekcji pojawi się opis tego, jak dostarczać do metody, oraz otrzymywać z niej, złożone typy danych.

Tagi: , , , , ,

Comments (1) -

eiter
eiter Poland
4/18/2010 7:24:37 PM Permalink

Dziękuje za wspaniałe podsumowania kursu!

Pingbacks and trackbacks (1)+

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading


Eastgroup.pl na facebooku