70-503: Defining Structural Contracts

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

Z poprzedniej lekcji wiemy jak utworzyć usługę i jak zdefiniować jej część “behawioralną”, czyli poszczególne metody serwisu. Dzisiaj dowiemy się jak zdefiniować własne typy danych i przekazywać je przez serwis.

Kontrakt danych

Kontrakt danych definiuje nam format, strukturę i sposób serializacji przesyłanych danych. Rozbudujmy nasz kalkulator o możliwość wykonywania dowolnych działań dwuargumentowych. Najpierw w pliku ICalculator.cs zdefiniujemy rodzaje działań, później obiekt zawierający wszystkie informacje potrzebne do wykonania działania.

   1: [DataContract(Namespace = "http://schemas.eastgroup.pl/2010/02/26/calculator/")]
   2: public enum CalculatorOperationType : int
   3: {
   4:     [EnumMember]
   5:     Add,
   6:     [EnumMember]
   7:     Substract,
   8:     [EnumMember]
   9:     Multiply,
  10:     [EnumMember]
  11:     Divide,
  12: }
  13:  
  14: [DataContract(Namespace = "http://schemas.eastgroup.pl/2010/02/26/calculator/")]
  15: public class CalculatorOperation
  16: {
  17:     [DataMember(IsRequired = true)] 
  18:     public CalculatorOperationType operation;
  19:  
  20:     [DataMember(IsRequired = true)] 
  21:     public double a;
  22:  
  23:     [DataMember(IsRequired = true)] 
  24:     public double b;
  25: }

DataContractAttribute jest zdefiniowany w System.Runtime.Serialization i może być użyty tylko do wyliczeń (enum), struktur i klas. Nie jest dziedziczony, więc każda klasa potomna, jeśli ma być typem danych przekazywanym przez serwis, powinna zawierać ten atrybut. Ma tylko dwa parametry:

  • Name – nazwa typu, np. jeśli chcemy nadać mu inną nazwę niż jest w definicji,
  • Namespace – przestrzeń nazw dla typu.

DataMemberAttribute jest też zdefiniowany w System.Runtime.Serialization i służy do oznaczenia pól lub właściwości, które mają być serializowane. Jako parametry możemy użyć:

  • Name – nazwa typu,
  • IsRequired – czy wartość dla pola jest wymagana czy nie,
  • Order – kolejność pól, domyślnie pola będą pokazane w kolejności alfabetycznej,
  • EmitDefaultValue – czy domyślne wartości mają być serializowane, jeśli true – wszystkie pola będą serializowane, jeśli false – serializowane będą tylko te pola, których wartość jest inna niż domyślna.

Ustawienie EmitDefaultValue na false może powodować problemy jeśli IsRequired jest ustawione na true, ponieważ serializer przekaże wartość domyślną, chociaż właściwie powinien w tym miejscu wystąpić wyjątek.

Jeszcze kilka dodatkowych uwag. Jeśli po klasie opatrzonej atrybutem DataContract coś dziedziczy, to pola i właściwości z klasy bazowej zawsze będą przed polami i właściwościami z klasy potomnej. Kolejne będą pola i właściwości klasy potomnej które nie posiadają parametru Order, a następnie te posiadające parametr Order.

EnumMemberAttribute – jak nazwa wskazuje, służy do deklaracji typów wyliczeniowych, jedynym parametrem jest:

  • Value – wartość, która ma być serializowana.

Teraz do kalkulatora możemy przekazywać paczkę z informacjami potrzebnymi do obliczeń:

   1: [ServiceContract(Name = "Calculator",
   2:     Namespace = "http://schemas.eastgroup.pl/2010/02/26/calculator/")]
   3: public interface ICalculator
   4: {
   5:     //(...)
   6:  
   7:     [OperationContract]
   8:     double BinaryOperation(CalculatorOperation calculatorOperation);
   9: }

Jeśli ktoś z Was używał XmlSerializer, to pewnie wie, że przy użyciu tej klasy serializowane są wszystkie pola oprócz tych oznaczonych NonSerializedAttribute. W przypadku DataContractSerializer’a jest odwrotnie: serializowane będą tylko pola opatrzone odpowiednim atrybutem (a pominięte te nieoznaczone żadnym atrybutem). Poniższy kod oddaje to o czym właśnie napisałem (pola PhoneNumber i EmailAddress będą serializowane, a HomeAddress nie):

   1: [Serializable()]
   2: public class ContactInfo
   3: {
   4:     public string PhoneNumber;
   5:  
   6:     public string EmailAddress;
   7:  
   8:     [NonSerialized()]
   9:     public string HomeAddress;
  10: }
  11:  
  12: [DataContract()]
  13: public class ContactInfo
  14: {
  15:     [DataMember()]
  16:     public string PhoneNumber;
  17:  
  18:     [DataMember()]
  19:     public string EmailAddress;
  20:     
  21:     public string HomeAddress;
  22: }

Kolekcje

W WCF’ie możemy używać dowolnych kolekcji (czyli wszystkiego implementującego IEnumerable lub IEnumerable<T>). Nasze dane będą reprezentowane jako tablica, informacje specyficzne dla danej kolekcji zostaną utracone. Np.:

   1: [ServiceContract()]
   2: interface ITaskManager
   3: {
   4:     [OperationContract()]
   5:     List<Task> GetTasksByAssignedName( string name);
   6: }

 

Po drugiej stronie serwisu będzie widoczny jako:

   1: [ServiceContract()]
   2: interface ITaskManager
   3: {
   4:     [OperationContract()]
   5:     Task[] GetTasksByAssignedName( string name);
   6: }

Będzie to obsłużone automatycznie, ale tylko wtedy gdy jest to konkretny typ kolekcji (nie interfejs) i jest on serializowalny (SerializableAttribute)

Co jeśli koniecznie chcemy by przekazywany obiekt pozostał kolekcją? Musimy użyć atrybutu CollectionDataContractAttribute:

   1: [CollectionDataContract(Name = "MyCollectionOf{0}")]
   2: public class MyCollection<T> : IEnumerable<T>
   3: {
   4:     public void Add(T item) { // Etc...
   5:     }
   6:  
   7:     IEnumerator<T> IEnumerable<T>.GetEnumerator() { // Etc...
   8:     }
   9:  
  10:     public IEnumerator GetEnumerator() { // Etc...
  11:     }
  12:  
  13:     // Etc...
  14: }
  15:  
  16:  
  17: [ServiceContract()]
  18: interface ITaskManager
  19: {
  20:     [OperationContract()]
  21:     MyCollection<Task> GetTasksByAssignedName(string name);
  22: }

Nasza kolekcja po drugiej stronie będzie listą:

   1: [CollectionDataContract]
   2: public class MyCollectionOfTask : List<Task>
   3: {}

Jeśli chodzi o typy danych to pozostał jeszcze jeden atrybut do omówienia: KnownTypeAttribute. Używamy go gdy chcemy skorzystać z polimorfizmu. Aby nie utracić informacji z klasy dziedziczącej musimy poinformować o tym, że po typie Task dziedziczy LoanApprovalTask i może on być przekazywany przez nasz serwis.

   1: [DataContract()]
   2: [KnownType(typeof(LoanApprovalTask))]
   3: class Task
   4: {
   5: }
   6:  
   7: [DataContract()]
   8: class LoanApprovalTask : Task
   9: {
  10: }

Kontrakt wiadomości

 

 

 

Mamy już opisane jakimi danymi się posługujemy. Teraz możemy przejść krok dalej i przejąć kontrolę nad całością przekazu. Jeśli chcemy mieć ustaloną przez nas strukturę wiadomości SOAP lub własne nagłówki (np. do przekazywania numeru licencji) to atrybuty MessageContractAttribute, MessageHeaderAttribute i MessageBodyMemberAttribute będą nam do tego potrzebne.

MessageContractAttribute jest używany do oznaczania klas definiujących strukturę komunikatu. Ma kilka parametrów:

  • IsWrapped – jeśli true to wszystko to co miało się znaleźć w ciele komunikatu będzie opakowane w element o nazwie klasy kontraktu lub nazwie podanej w parametrze WrapperName
  • ProtectionLevel – rodzaj zabezpieczeń (podpis, szyfrowanie, …), będzie omówione w kolejnych lekcjach
  • WrapperName – nazwa elementu opakowującego zawartość ciała komunikatu
  • WrapperNamespacenamespace dla elementu opakowującego ciało komunikatu

MessageHeaderAttribute służy do oznaczenia elementów, które mają wejść w skład nagłówka. Ma 6 parametrów:

  • Name – nazwa serializowanego elementu nagłówka
  • Namespace – przestrzeń nazw elementu
  • ProtectionLevel – rodzaj zabezpieczeń
  • Actor – URI wskazujące aktora – cel nagłówka, domyślnie usługa odbierająca
  • MustUnderstand – czy aktor musi zrozumieć nagłówek (jeśli nie zrozumie a ten parametr jest ustawiony na true to aktor musi zgłosić błąd)
  • Relay – czy nagłówek powinien być przekazany do kolejnego odbiorcy jeśli wiadomość nie została przetworzona przez aktora

A atrybutem MessageBodyMemberAttribute oznaczamy elementy, które mają się znaleźć w ciele wiadomości. Parametry:

  • Name – nazwa serializowanego elementu ciała wiadomości
  • Namespace – przestrzeń nazw elementu
  • ProtectionLevel – rodzaj zabezpieczeń
  • Order – kolejność elementów (podobnie jak przy DataMemberAttribute)

Poniżej przykład kodu (myślę że nie wymaga wyjaśniania):

   1: [DataContract()]
   2: public class ContactInfo
   3: {
   4:     [DataMember()]
   5:     public string PhoneNumber;
   6:  
   7:     [DataMember()]
   8:     public string EmailAddress;
   9: }
  10:  
  11: [MessageContract(IsWrapped = false)]
  12: public class ContactInfoRequestMessage
  13: {
  14:     [MessageHeader()]
  15:     public string LicenseKey;
  16: }
  17:  
  18: [MessageContract(IsWrapped = false)]
  19: public class ContactInfoResponseMessage
  20: {
  21:     [MessageBodyMember()]
  22:     public ContactInfo ProviderContactInfo;
  23: }
  24:  
  25: [ServiceContract()]
  26: public interface ISomeService
  27: {
  28:     [OperationContract()]
  29:     [FaultContract(typeof(string))]
  30:     ContactInfoResponseMessage GetProviderContactInfo(
  31:         ContactInfoRequestMessage reqMsg);
  32: }
  33:  
  34: public class SomeService : ISomeService
  35: {
  36:     public ContactInfoResponseMessage GetProviderContactInfo(
  37:         ContactInfoRequestMessage reqMsg)
  38:     {
  39:         if (reqMsg.LicenseKey != ValidLicenseKey)
  40:         {
  41:             const string msg = "Invalid license key.";
  42:             throw new FaultException<string>(msg);
  43:         }
  44:         
  45:         ContactInfoResponseMessage respMsg =
  46:             new ContactInfoResponseMessage();
  47:         respMsg.ProviderContactInfo = new ContactInfo();
  48:         respMsg.ProviderContactInfo.EmailAddress = "sam@fabrikam.com";
  49:         respMsg.ProviderContactInfo.PhoneNumber = "123-456-7890";
  50:         return respMsg;
  51:     }
  52:     
  53:     private const string ValidLicenseKey = "abc-1234-alpha";
  54: }

Wersjonowanie

Jedną z głównych zalet udostępniania usług w postaci serwisów jest odseparowanie klienta od naszej usługi. Ale co zrobimy gdy będzie coś zmienić…

Przy konieczności dodania czegoś do wiadomości DataContractSerializer jest na tyle uprzejmy że jeśli czegoś się nie spodziewa w odebranej paczce to po prostu to ignoruje.

Przy braku oczekiwanego elementu jeśli nie był on wymagany (IsRequired = false) serializer przyjmie wartość null dla typów referencyjnych lub wartości zerowe dla pozostałych. Jeśli element był wymagany, nastąpi wyjątek.

Co w przypadku gdy serwis został rozszerzony a użytkownik ma jeszcze starego klienta i musi przynajmniej zapamiętać dane które dostał z serwisu? A jeśli te dane ma odesłać bo to np. dane autoryzacyjne? Tutaj przychodzi z pomocą IExtensibleDataObject, który umożliwi nam obsługę “nieznanych” danych.

   1: [DataContract(Namespace =
   2:     "http://schemas.fabrikam.com/2008/04/tasks/")]
   3: public class Task : IExtensibleDataObject
   4: {
   5:     [DataMember(IsRequired = true, Order = 1)]
   6:     public string Description;
   7:     // Etc...
   8:  
   9:     public ExtensionDataObject ExtensionData
  10:     {
  11:         get
  12:         {
  13:             return _extensionData;
  14:         }
  15:  
  16:         set
  17:         {
  18:             _extensionData = value;
  19:         }
  20:     }
  21:  
  22:     private ExtensionDataObject _extensionData;
  23: }

W obiekcie _extensionData będą przechowywane wszystkie dane, które klient nie potrafił obsłużyć. Możemy je sobie gdzieś zachować i przy aktualizacji aplikacji z nich skorzystać.

Kontrola serializacji

 

Na koniec jeszcze krótko o samej serializacji. Do dyspozycji mamy 2 serializery: DataContractSerializer i XmlSerializer i 3 enkodery: enkoder tekstowy dla standardowego XML’a, enkoder MTOM oraz binarny WCF-to-WCF (będą opisane w kolejnych lekcjach).

WCF udostępnia dwa atrybuty mówiące o tym który serializer ma być użyty: XmlSerializerFormatAttribute i DataContractFormatAttribute. Oba mają podobne parametry:

  • StyleRpc (remote procedure call) lub Document – styl formatowania SOAP,
  • UseLiteral lub Encoded – czy wstawiać dane “dosłownie” czy jako zakodowane.
   1: [ServiceContract()]
   2: [XmlSerializerFormat(Style=OperationFormatStyle.Rpc,
   3:     Use=OperationFormatUse.Encoded)]
   4: interface ISomeLegacyService
   5: {
   6:     [OperationContract()]
   7:     string SomeOp1( string name);
   8: }
   9:  
  10: [ServiceContract()]
  11: [DataContractFormat(Style=OperationFormatStyle.Rpc)]
  12: interface ISomeRpcService2
  13: {
  14:     [OperationContract()]
  15:     string SomeOp2( string name);
  16: }

Na dzisiaj tyle, w kolejnym artykule pokażemy jak udostępnić nasz serwis światu.

Tagi: , , , , , ,

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading