70-503: Concurrency in WCF Applications

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

Współbieżność (ang. concurrency) w serwisie WCF występuje, kiedy jednocześnie więcej niż jedno wywołanie ma miejsce. Celem serwisu WCF jest przetwarzanie przychodzących żądań. Kiedy żądanie przychodzi do serwisu, serwis rozdziela (ang. dispatch) komunikaty na własne wątki, które brane są z puli wątków. Z każdym żądaniem powiązany jest obiekt serwisu – instancja klasy, która implementuje interfejs serwisu. W WCF kwestia współbieżności zależy od tego, jak te obiekty są tworzone i dzielone pomiędzy pojedyncze żądania.

WCF przewiduje trzy możliwe tryby dzielenia obiektu serwisu:

  • Single – każdy wątek, który obsługuje żądanie może mieć dostęp do obiektu serwisu, ale tylko jeden w danym czasie, używanie trybu Single zmniejsza ilość problemów związanych z współbieżnością,
  • Reentrant – tylko jeden wątek może mieć dostęp do obiektu serwisu w danym czasie, jednak ma on możliwość opuszczenia obiektu i powrotu do niego w późniejszym czasie,
  • Multiple – obiekt serwisu obsługuje jednocześnie wiele żądań; jest to najtrudniejszy tryb do implementacji, ponieważ wymaga wielkiej staranności przy korzystaniu z zasobów dzielonych (ang. shared resources).

Tryb współbieżności jest ustawiany korzystając z atrybutu ServiceBehavior - ConcurrenczMode, na klasie, która implementuje serwis.

Tryb współbieżności Single

Ustawienie ConcurrencyMode na wartość ConcurrencyMode.Single gwarantuje najbardziej bezpieczne środowisko dla współbieżności.

   1: [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Single)]
   2: public class ServiceImplementation : IServiceInterface
   3: {
   4:   // TODO: Implementacja...
   5: }

Przed rozpoczęciem przetwarzania żądania, na obiekt serwisu zakładana jest blokada, która zdejmowana jest dopiero po zakończeniu operacji. Jeżeli w tym czasie przyjdą kolejne żądania, zostaną one odłożone na kolejkę (FIFO – first-in, first-out) i czekają, kiedy obiekt serwisu będzie dostępny. Przetwarzanie pojedynczego żądania w danym czasie eliminuje problemy zarządzanie współbieżnością. Jedyną sytuacją, kiedy może pojawić się problem jest sytuacja, gdy obiekt serwisu wykonuje operacje wielowątkowe. Istnieje tu jednak pewien kompromis, którym zależy od trybu wystąpienia. Innymi słowy należy uwzględnić związek pomiędzy ConcurrencyMode a InstanceContextMode.

Przykład: Zakładamy, że mamy podaną gotową implementację serwisu. Jak możemy udekorować klasę, aby wyeliminować współbieżność, nie modyfikując niczego wewnątrz klasy?

   1: [ServiceBehavior()]
   2: public class ServiceImplementation : IServiceInterface
   3: {
   4:   private static int hitCounter;
   5:   public void Increment()
   6:   {
   7:     hitCounter++;
   8:   }
   9: }

Możemy ustawić ConcurrencyMode na Single a InstanceContextMode na PerSession lub Single. Wykorzystanie trybu współbieżności Single zapewnia, że tylko jedno żądanie może być przetwarzane w danej chwili. Połączenie ConcurrencyMode w trybie Single i InstanceContextMode w trybie Single daje gwarancję, że jest tworzona tylko jedna instancja serwisu. To połączenie jest wymagane w przypadku operacji ze statycznymi lub dzielonymi zmiennymi, aby były zabezpieczone na naruszenia współbieżności.

Tryb Single cechuje potencjalnie niska przepustowość spowodowana przesyłaniem przychodzących żądań przez jeden obiekt.

Tryb współbieżności Multiple

W przypadku serwisów, które wymagają większej przepustowości dostępny jest model wielowątkowy. Kiedy ConcurrencyMode jest ustawiony na Multiple, nie występuje już zakładanie blokady na obiekt serwisu przed obsłużeniem żądania, a obiekt serwisu może (w zależności od trybu wystąpienia) obsłużyć wiele żądań jednocześnie. Informacja o stanie serwisu i dzielonych zasobach musi być chroniona poprzez wykorzystanie standardowych technik synchronizacji, które oferuje .NET framework.

Tryb współbieżności Reentrant

Serwis po ustawienie trybu na Reentrant zachowuje się tak samo jak w przypadku trybu Single. Przed przetworzeniem żądania zakładana jest blokada na serwis i trzymana jest tak długo, jak długo trwają operacje. Różnica leży w tym, co może się wydarzyć podczas samego przetwarzania.

imagePodczas przetwarzania żądania przez serwis może wystąpić sytuacja, że serwis musi wykonać operację na innym serwisie. Inne żądania do serwisu muszą czekać na zakończenie aktualnie przetwarzanego przez serwis żądania, gdzie serwis czeka na zakończenie wywołania, które sam rozpoczął. Problem ten ilustruje zamieszczony rysunek.

Co się teraz stanie, jeżeli zewnętrzny serwis wywoła żądanie na naszym serwisie WCF? Żądanie zostanie dodane do kolejki, obiekt serwisu nie zostanie odblokowany, wystąpi zakleszczenie! (WCF jest w stanie poradzić sobie z tym przez unieważnienie żądanie po pewnym czasie – timeout, lub rzucenie wyjątku InvalidOperationException). Tryb Reentrant rozwiązuje ten problem.

Różnica między trybem Single a Reentrant jest taka, że w tym drugim trybie, kiedy obiekt serwisu wykonuje własne żądanie zdejmowana jest blokada. Pozwala to na obsłużenie innych żądań. Kiedy odpowiedz na żądanie serwisu nadejdzie, zostanie dodana do kolejki razem z innymi żądaniami. Kiedy zacznie się jego przetwarzanie, założy blokadę na obiekt serwisu i dokończy swoje zadanie.

Chociaż konfiguracja serwisu do działania w trybie reentrant jest prosta, programista musi musi liczyć się z dużą odpowiedzialnością wiążącą się z tym rozwiązaniem. Zakleszczenia są problemem, jednak nie jedynym. Programista musi zapewnić, że kiedy serwis wykonuje żądanie, stan serwisu musi pozostać spójny – serwis nie powinien oddziaływać ze swoimi własnymi polami (publicznymi, czy prywatnymi, należącymi do instancji, czy statycznymi) w taki sposób, aby jakikolwiek obiekt pozostał w nieakceptowanym stanie. Przykładowo, jeżeli budujemy strukturę drzewiastą, nie możemy dopuścić do tego, aby korzeń drzewa nie był zdefiniowany.

W następnej, ostatniej już lekcji, zapoznamy się z synchronizacją.

Tagi: , , , , ,

70-503: Transaction Basics

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

Podstawową funkcją transakcji jest zagwarantowanie zasad ACID:

  • atomowości (ang. atomicity),
  • spójności (ang. consistency),
  • izolacji (ang. isolation),
  • trwałości (ang. durability).

Kiedy operacje związane z bazą odbywają się na wielu maszynach i wielu zbiorach danych, nie jest to takie proste. WCF wspomaga programistę w tym zadaniu.

W celu spełnienia zasad ACID najczęstszym podejściem jest wykorzystanie dwuetapowego zgłoszenia (ang. two-phase commit):

  1. Etap przygotowania (ang. prepare phase) – koordynator transakcji zarządza tym etapem, wysyła żądanie przygotowania do wszystkich zarządców transakcji (ang. transaction manager), na maszynach biorących udział w procesie. Zarządcy odsyłają informację o tym, czy operacje zakończyły się sukcesem, czy porażką. Kiedy wszyscy odpowiedzą etap przygotowania uznajemy za zakończony.
  2. Etap zgłoszenia (ang. commit phase) – zależny od wyników etapu poprzedniego; jeżeli tamten zakończy się sukcesem – wysyłane zostaje żądanie Commit, w przeciwnym wypadku, jeżeli chociaż jedna maszyna zwróci komunikat błędu – koordynator transakcji wysyła żądanie Abort, aby poinformować zarządców o potrzebie cofnięcia zmian.

W zależności od sytuacji wykorzystany może zostać jeden z trzech zarządców transakcji:

  • The Lightweight Transaction Manager (LTM) – wprowadzony w .NET 2.0 przez przestrzeń nazw System.Transaction (do projektu trzeba dodać assembly); poniższy przykład aktualizuje bazę danych w ramach lekkiej transakcji. W celu zgłoszenia transakcji wywołana jest metoda Complete:
  •    1: using (TransactionScope ts = new TransactionScope())
       2: {
       3:   using (SqlConnection cn1 = new SqlConnection(connectionString))
       4:   {
       5:     insertRecord(cn1, "User1");
       6:     using(SqlConnection cn2 = new SqlConnection(connectionString))
       7:     {
       8:       insertRecord(cn2, "User2");
       9:     }
      10:   }
      11:   ts.Complete();
      12: }
      13: private void insertRecord(SqlConnection cn, string userName)
      14: {
      15:   SqlCommand cmd = new SqlCommand(String.Format("Insert INTO [Users]" +" VALUES('{0}')", userName), cn);
      16:   cn.Open();
      17:   cmd.ExecuteNonQuery();
      18: }
  • OLE Transactions (OleTx),
  • WS-Atomic Transactions (WS-AT).

Transakcji możemy pozwolić na działanie poza granicami serwisu, lub nie. Decyzję o tym podejmują klient i serwis, jednak jeżeli serwis wymaga transakcji, blokowanie po stronie klienta spowoduje błąd aplikacji. Ustawione może to zostać w bindingu za pomocą atrybutu TransactionFlow, zarówno imperatywnie jak i deklaratywnie:

   1: // C#
   2: WSHttpBinding binding = new WSHttpBinding();
   3: binding.TransactionFlow = true;
   4:  
   5: <!--XML-->
   6: <bindings>
   7: <wsHttpBinding>
   8: <binding name="Transactional" transactionFlow="true" />
   9: </wsHttpBinding>
  10: </bindings>

 

 

Dodatkowo wymagane jest, aby operacje serwisu były oznaczone atrybutem TransactionFlow, który wskazuje na to, że mogą one brać udział w transakcji:

   1: [ServiceContract]
   2: public interface IDemoContract
   3: {
   4:   [OperationContract]
   5:   [TransactionFlow(TransactionFlowOption.Allowed)]
   6:   void TransactedMethod(...);
   7: }

Po stronie klienta w ten sam sposób zostanie udekorowana klasa proxy:

   1: public class DemoService : IDemoContract
   2: {
   3:   [TransactionFlow(TransactionFlowOption.Allowed)]
   4:   public void TransactedMethod(...)
   5:   {...}
   6: }

W powyższym przykładzie TransactionFlow ustawiony jest na wartość TransactionFlowOption.Allowed, która pozwala transakcji klienta przechodzić do serwisu. Domyślna opcja TransactionFlowOption.NotAllowed sprawia, że żadna transakcja nie jest przesyłana.

Uwaga: Transakcje nie działają w przypadku metod typu one-way – bez komunikatu zwrotnego nie ma możliwości stworzenia rozproszonej transakcji.

Tagi: , , , ,

70-503: Instancing Modes

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

WCF jest odpowiedzialny za wiązanie przychodzącego komunikatu do określonej instancji serwisu. Tryb wystąpienia (ang. instance mode) określa związek pomiędzy klientem a instancją serwisu (np. czy istniejąca instancja serwisu jest w stanie przetworzyć żądanie). Ta lekcja przedstawia, różne rodzaje możliwych wystąpień, sposób w jaki są tworzone i konsekwencje wyborów.

Dla InstanceContextMode dostępne są trzy wybory: per call mode, per session mode oraz singleton mode.

Co wywołanie

Domyślnym trybem jest PerCall, który utrzymuje asocjację jeden-do-jednego pomiędzy wywołaniami metod a instancjami serwera. Każde pojedyncze żądanie dostaje swoją własną kopię serwisu (obiektu serwisu). Klient wywołuje żądanie do serwisu poprzez obiekt proxy. Kiedy żądanie dochodzi do serwisu, host tworzy instancję serwisu. Obiekt ten jest następnie wywoływany w celu przetworzenia tego żądania. Kiedy zostanie ono już przetworzone a odpowiedź zwrócona do klienta, obiekt jest usuwany.

image

Jakkolwiek jest to metoda prosta w użyciu, nie oznacza to, że zawsze jest najlepsza. Ograniczony zasób (ang. scarce resource) to taki, który jest kosztowny u utworzeniu, lub ilość jego wystąpień jest ograniczona. Przykłady takich zasobów to pliki na dysku twardym komputera, połączenia do bazy danych, połączenia sieciowe, komunikacja przez porty. W takim wypadku tylko jedna instancja serwisu jest w stanie korzystać z zasobu, pozostałe muszą czekać na swoją kolej.

Tryb wystąpienia ustawiany jest po stronie serwisu. Poniższy przykład demonstruje sposób, w jaki można ustawić tryb na PerCall:

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
   2: class UpdateService : IUpdateService {...}

W tym trybie nie przekazywany jest stan aplikacji między wywołaniami metod. Ogólnie tryb ten najlepiej sprawdzi się kiedy indywidualne operacje są szybkie i nie składają się z żadnych zadań w tle, które kontynuują działanie nawet po obsłużeniu żądania.

Tryb sesji

image Tryb PerSession tworzy instancję dla każdego proxy klienta. W tym trybie WCF utrzymuje prywatną sesję pomiędzy klientem a określoną instancją serwisu. Klient, podczas pierwszego żądania do serwisu, otrzymuje instancję serwisu, która jest przeznaczona do obsłużenia żądań tego klienta. Wszystkie następujące potem żądania danego klienta są uznawane za część tej samej sesji, a wywołania są przetwarzane przez tę samą instancję serwisu.

Aby korzystać z tego trybu właściwość SessionMode musi zostać ustawiona na SessionMode.Required, co spowoduje poinformowanie klienta, że sesja ma być prowadzona:

   1: [ServiceContract(SessionMode = SessionMode.Required)]
   2: public interface IUpdateService
   3: {
   4: // ...
   5: }

Druga część konfiguracji to odpowiednie ustawienie InstanceContextMode:

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
   2: public class UpdateService : IUpdateService
   3: {
   4: // ...
   5: }

Instancja serwisu istnieje dopóki klient jej potrzebuje. Kiedy zakończy korzystanie ma możliwość zamknięcia proxy. Jeżeli tego nie zrobi sesja zakończy się automatycznie po dziesięciu minutach nieaktywności. Po tym czasie żądanie do serwisu spowoduje wyrzucenie wyjątku CommunicationObjectFaultedException. Przykład zmiany tej wartości dla netTcpBinding:

   1: NetTcpBinding binding = new NetTcpBinding();
   2: binding.ReliableSession.Enabled = true;
   3: binding.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(60);

Przykład deklaratywny:

   1: <netTcpBinding>
   2:  <binding name="timeoutSession">
   3:   <reliableSession enabled="true" inactivityTimeout="01:00:00"/>
   4:  </binding>
   5: </netTcpBinding>

Uwaga: basicHttpBinding nie jest w stanie przekazać informacji związanej z sesją!

Tryb pojedynczego wystąpienia (Singleton)

W tym trybie tworzona jest tylko jedna instancja serwisu. Obsługuje ona każde żądanie, które dociera do serwisu. Instancja nie jest usuwana, trwa do wyłączenia procesu hostującego go. Tryb ten nie wymaga danych sesyjnych podczas przesyłania komunikatów. Przykład ustawienia trybu:

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
   2: public class UpdateService : IUpdateService
   3: {
   4: // ...
   5: }

Najlepsze zastosowania tego trybu to takie, które modelują pojedynczy zasób – plik dziennika zdarzeń (log), komunikacja z pojedynczym robotem.

Podsumownie

Kiedy należy odpowiedzieć na pytanie, który tryb wystąpienia jest najlepszy, należy stwierdzić, że nie ma jednej poprawnej odpowiedzi. Wiele czynników musi zostać rozważonych, aby określić, “ten odpowiedni”. Wybór uzależniony jest od konkretnego zastosowania.

Tagi: , , , ,

70-503: Dealing with POX

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

Istnieje oraz zapewnie będzie istniała nadal potrzeba tworzenia serwisów, które mogą otrzymywać komunikaty w postaci dokumentu Plain old XML (POX). Serwis musi umieć sprawdzić taki komunikat bez dokładniej wiedzy na temat jego struktury. Wiele aplikacji klienckich nie potrafi utworzyć komunikatu sformatowanego jako SOAP. Przykładowo przeglądarki internetowe nie mają wsparcia dla protokołów opartych na SOAP. Potrafią one jedynie wykonać żądanie HTTP. W tych wypadkach treścią żądania jest zwykły ciąg znaków, lub XML. W takim wypadku serwis musi umieć zaakceptować komunikat, bez względu na jego format, po czym należy obsłużyć go ręcznie. Po sprawdzeniu go możemy podjąć decyzję na temat przekazania go w inne miejsce.

Dla WCF obsługa tej technologii to zdolność do akceptacji opcjonalnego komunikatu (ang. arbitrary message) w przeciwieństwie do tych ściśle określonych. Kluczową rolę ogrywa tu klasa Message, która zapewnia dostęp do informacji. W celu obsłużenia komunikatu pozbawionego typu można wykorzystać wartości Action i SoapAction, które mogą zostać załączone w żądaniu. SoapAction wskazuje nazwę metody serwisu, która ma zostać wywołana. Dotyczy komunikatów w formacie SOAP. Action dostarcza URI, który ma wskazać na akcję serwisu. Możemy ręcznie przypisać go do konkretnej akcji:

   1: [ServiceContract]
   2: public interface IMessageHandler
   3: {
   4:   [OperationContract(Action="uri://service/description")]
   5:   Message HandleThisMessage(Message request);
   6: }

Parametr ten nie jest wymagany i może się okazać, że komunikat, który dotrze do serwisu nie będzie go posiadał. Poprzez ustawienie właściwości Action na gwiazdkę, możemy obsłużyć wszystkie nierozpoznane lub nieistniejące akcje. Przypiszemy je do konkretnej metody:

   1: [ServiceContract]
   2: public interface IMessageHandler
   3: {
   4:   [OperationContract(Action="*")]
   5:   Message HandleAllMessages(Message request);
   6: }

Zwracany komunikat ma typ Message. Jeżeli nie potrzebujemy odsyłania odpowiedzi do klienta możemy ustawić parametr IsOneWay na true:

   1: [ServiceContract]
   2: public interface IMessageHandler
   3: {
   4:   [OperationContract(Action="*", IsOneWay=true)]
   5:   void HandleAllMessages(Message request);
   6: }

Ciało (ang. body) obiektu typu Message może zostać przetworzone wyłącznie raz. Po przetworzeniu stan zostanie ustawiony na wartość Read, a następne próby dostępu spowodują wyrzucenie wyjątku. Ciało komunikatu możemy otrzymać korzystając z metody GetReaderAtBodyContents, która zwraca obiekt typu XmlReader. Komunikat ma właściwość State, która wskazuje jego stan. Na początku jest to Created, po przeczytaniu zamieniany jest na Read. Przykład czytania:

   1: Message HandleMessage(msg As Message)
   2: {
   3:   XmlReader body = msg.GetReaderAtBodyContents();
   4:   while (body.Read())
   5:   {
   6:     string bodyText = body.ReadString();
   7:     // Process the body
   8:   }
   9:   body.Close();
  10:   // Rest of processing
  11: }

Alternatywnym sposobem jest wykorzystanie metody GetBody, która przyjmuje jako parametr klasę .NET, która została użyta, kiedy komunikat był tworzony po stronie klienta:

   1: public void HandleAllMessage(Message request)
   2: {
   3:   Customer c = request.GetBody<Customer>();
   4:   // Process the incoming customer object
   5: }

Ciało komunikatu nie jest jedyną częścią, którą możemy się zainteresować. Przykładowo metod IsEmpty sprawdzi, czy ciało istnieje, IsFault, czy jest to komunikat SOAP typu fault, właściwość Version poda wersję SOAP. Właściwość Header zawiera listę nagłówków powiązanych z komunikatem. Ma ona typu MessageHeaders, a jest to kolekcja obiektów MessageHeaderInfo. Poszczególny nagłówek możemy otrzymać przez indeks, lub iterując po kolekcji. Właściwości klasy MessageHeaderInfo, to:

  • Actor – zamierzony cel nagłówka,
  • IsReferenceParameter – określa, czy parametr jest powiązany z elementem ReferenceParameters w specyfikacji WS-Addressing,
  • MustUnderstand – określa, czy serwis przetwarzający komunikat musi rozumieć nagłówek,
  • Name – nazwa nagłówka komunikatu,
  • Namespace – przestrzeń nazw dla nagłówka komunikatu,
  • Relay – określa, czy nagłówek może zostać przekazany dalej.

Tworzenie komunikatu

Omówiliśmy przetwarzanie komunikatów. Innym zagadnieniem jest ich tworzenie. Poniższy przykład demonstruje przebieg tego procesu:

   1: Customer c = new Customer();
   2: c.Name = "Contoso";
   3: c.City = "Redmond";
   4: MessageVersion version = MessageVersion.Soap12;
   5: Message m = Message.CreateMessage(version, "HandleAllMessage", c);

Wykorzystana została tu statyczna metoda klasy MessageCreateMessage.

Tagi: , , ,

70-503: Authentication

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

W tej lekcji zajmiemy się tematyką uwierzytelniania – określaniem kto jest kim (potwierdzaniem tożsamości), czy szyfrowaniem. Uwierzytelnianie będzie obejmowało zarówno weryfikację klienta przez serwis, jak i serwisu przez klienta. WCF oferuje następujące mechanizmy uwierzytelniania:

  • Brak uwierzytelniania (No authentication) – dostęp anonimowy bez potwierdzania tożsamości,
  • Uwierzytelnianie Windows (Windows authentication) – Kerberos dla serwisów działających w ramach domeny Windows, Windows NT LAN (NTLM) dla grup roboczych i samodzielnych systemów,
  • Nazwa użytkownika i hasło (Username and password) – klient dostarcza dane, serwis uwierzytelnia klienta na ich podstawie,
  • Certyfikat X.509 (X.509 certificate) – klient identyfikuje się korzystając z certyfikatu,
  • Wydane żetony (Issued tokens) – klient przekazuje żeton do serwisu, aby zidentyfikować nadawcę,
  • Niestandardowy (Custom) – wykorzystanie własnych mechanizmów uwierzytelniania, np. danych biometrycznych.

Dane uwierzytelniające klienta

Podczas uwierzytelniania klienta należy określić, które informacje powinny zostać wykorzystane oraz jak powinny zostać dostarczone do serwisu. Możliwe wartości to: Certificate, IssuedToken, None, UserName i Windows. Nie wszystkie wartości są jednak odpowiednie dla wszystkich bindingówbasicHttpBinding wspiera jedynie opcje UserName i Certificate. Przykład deklaratywnego określenia typu uwierzytelniających danych:

   1: <binding name="myBinding">
   2:   <security mode="Message">
   3:     <message clientCredentialType="Certificate"/>
   4:   </security>
   5: </binding>

Uwierzytelnianie certyfikatem

Przykładowa konfiguracja przy uwierzytelnianiu klienta certyfikatem:

   1: <behaviors>
   2:  <serviceBehaviors>
   3:   <behavior name="ServiceCredentialsBehavior">
   4:    <serviceCredentials>
   5:     <serviceCertificate findValue="Contoso.com" x509FindType="FindBySubjectName" />
   6:    </serviceCredentials>
   7:   </behavior>
   8:  </serviceBehaviors>
   9: </behaviors>

Element serviceCredentials zawiera informacje na temat tego, jak klient będzie uwierzytelniany przez serwis.

Uwierzytelnianie żetonem

Główna idea stojąca za uwierzytelnianiem za pomocą żetonu jest pozwolenie firmom trzecim na potwierdzenie tożsamości klienta. W środowisku .NET przykładem wykorzystania jest usługa CardSpace. Przykładowy kod pliku konfiguracyjnego:

   1: <services>
   2:  <service name="UpdateService" behaviorConfiguration="ServiceCredentials">
   3:   <endpoint address="" binding="wsHttpBinding" bindingConfiguration="requireInfoCard" contract="IUpdateService" >
   4:    <identity>
   5:     <certificateReference findValue="545c3b8e97d99fd75c75eb52c6908320088b4f50" x509FindType="FindByThumbprint" storeLocation="LocalMachine" storeName="My" />
   6:    </identity>
   7:   </endpoint>
   8:  </service>
   9: </services>
  10:  
  11: <bindings>
  12:  <wsHttpBinding>
  13:   <binding name="requireInfoCard">
  14:    <security mode="Message">
  15:     <message clientCredentialType="IssuedToken" />
  16:    </security>
  17:   </binding>
  18:  </wsHttpBinding>
  19: </bindings>
  20:  
  21: <behaviors>
  22:  <serviceBehaviors>
  23:   <behavior name="ServiceCredentials">
  24:    <serviceCredentials>
  25:     <serviceCertificate findValue="545c3b8e97d99fd75c75eb52c6908320088b4f50" x509FindType="FindByThumbprint" storeLocation="LocalMachine" storeName="My" />
  26:     <issuedTokenAuthentication allowUntrustedRsaIssuers="true" />
  27:    </serviceCredentials>
  28:   </behavior>
  29:  </serviceBehaviors>
  30: </behaviors>

Element identity w sekcji endpoint pozwala różnym punktom końcowym dostarczać funkcjonalność uwierzytelniania dla serwisu. W tym przykładzie element certificateReference dostarcza szczegóły na temat tego, jakie mechanizmy uwierzytelniania zostaną wykorzystane.

Uwierzytelnianie systemu Windows

Uwierzytelnianie Windows jest domyślnie wykorzystywane do uwierzytelnianie klienta przy zabezpieczeniach na poziomie wiadomości. Ustawić ten typ uwierzytelniania można zarówno deklaratywnie, jak i imperatywnie. Poniżej przykład deklaratywny:

   1: <bindings>
   2:  <wsHttpBinding>
   3:   <binding name="messageSecurity">
   4:    <security mode="Message">
   5:     <message clientCredentialType="Windows"/>
   6:    </security>
   7:   </binding>
   8:  </wsHttpBinding>
   9: </bindings>

Przykład imperatywny:

   1: UpdateServiceClient proxy = new UpdateServiceClient();
   2: proxy.ClientCredentials.Windows.ClientCredential = CredentialCache.DefaultCredentials;
   3: proxy.ClientCredentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation;
   4: proxy.ClientCredentials.Windows.AllowNtlm = false;

Atrybut allowImpersonationLevel określa typ podszywania się, które serwis może wykonać. Więcej na ten temat będzie można przeczytać w następnej lekcji.

Dane uwierzytelniające serwisu

Wspomniana była już możliwość wysyłania danych uwierzytelniających przez serwis do klienta. Przykładowa konfiguracja:

   1: <behaviors>
   2:  <serviceBehaviors>
   3:   <behavior name="serviceBehavior" >
   4:    <serviceCredentials>
   5:     <serviceCertificate findValue="RPKey" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
   6:    </serviceCredentials>
   7:   </behavior>
   8:  </serviceBehaviors>
   9: </behaviors>

Jeżeli klient potrzebuje zaszyfrować komunikat wysyłany do serwisu, musi mieć klucz publiczny certyfikatu. Może on zostać dostarczony ręcznie lub dostarczony na drodze negocjacji. Zachowanie to można ustawić za pomocą atrybutu negotiateServiceCredential, w elemencie message bindingu:

   1: <wsHttpBinding>
   2:  <binding name="wsHttp">
   3:   <security mode="Message">
   4:    <message clientCredentialType="Certificate" negotiateServiceCredential="true" />
   5:   </security>
   6:  </binding>
   7: </wsHttpBinding>

Uwierzytelnianie niestandardowe

Pomimo tego, że wbudowane metody uwierzytelnianie są pomocne, nie ma możliwości, aby były wystarczające w każdym możliwym przypadku. Dla takich przypadków mamy możliwość dostarczenia własnych mechanizmów uwierzytelniania. W tym celu tworzymy obiekt dziedziczący po UserNamePasswordValidator, w który przeciążamy metodę Validate. Jeżeli metoda się zakończy oznacza to, że dostarczone dane są poprawne. Jeżeli dane nie są poprawne należy zgłosił wyjątek SecurityTokenValidationException. Przykład:

   1: public class CustomAuthenticator : UserNamePasswordValidator
   2: {
   3:  public override void Validate(string userName, string password)
   4:  {
   5:   if (userName != "anyuser" || password != "good")
   6:     throw new SecurityTokenValidationException("Invalid credentials");
   7:  }
   8: }

Po utworzeniu klasy walidatora należy skonfigurować serwis tak, aby z niej korzystał:

   1: <serviceBehaviors>
   2:  <behavior name="CustomValidator">
   3:   <serviceCredentials>
   4:    <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="ThisAssembly.CustomAuthenticator, ThisAssembly"/>
   5:    <serviceCertificate findValue="localhost" x509FindType="FindBySubjectName" storeLocation="CurrentUser" storeName="My" />
   6:   </serviceCredentials>
   7:  </behavior>
   8: </serviceBehaviors>

Tagi: , , , ,

70-503: Transport-Level Security

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

Bezpieczeństwo tworzonych serwisów to podstawowa sprawa, która powinna odgrywać znaczącą już od samego początku projektu. W tym artykule skupimy się na bezpieczeństwie związanym z infrastrukturą – jak ograniczyć dostęp nieuwierzytelnionym użytkowników. Większość bindingów ma wbudowane możliwości związane z bezpieczeństwem – może to być SSL, IPsec, może też ich nie być wcale. wsDualHttpBinding to przykładowy binding, który nie wspiera zabezpieczeń na poziomie transportu. Wiąże się to z zasadą jego działania.

Artykuł ten skupia się na możliwościach WCF związanych z bezpieczeństwem, oraz biningach, które je obsługują.

Główne zadania bezpieczeństwa w WCF, to zagwarantowanie:

  • Uwierzytelnienia nadawcy
  • Uwierzytelnienie serwisu
  • Spójność komunikatu
  • Tajność komunikatu
  • Rozpoznawanie odpowiedzi

Bezpieczeństwo oferowane przez WCF wykorzystuje wiele istniejących rozwiązań, jak np. SSL, Kerberos, czy Active Directory Domain Services (AD DS). To, które z nich są wykorzystywane zależy od wybranego bindingu.

basicHttpBinding

Celem stworzenia basicHttpBinding jest zapewnienie kompatybilności z istniejącymi rozwiązaniami, takimi jak ASP.NET Web Services, czy WSE. Jest to jedyne rozwiązanie, które nie jest zabezpieczone w domyślnej konfiguracji. Zachowanie to można jednak zmienić zarówno w kodzie, jak i w pliku konfiguracyjnym. Poniższy przykład przedstawia deklaratywny sposób konfiguracji:

   1: <basicHttpBinding>
   2:   <binding name="TransportBinding">
   3:     <security mode="Transport">
   4:       <transport clientCredentialType="Basic"
   5:         proxyCredentialType="Basic"
   6:         realm="contoso" />
   7:     </security>
   8:   </binding>
   9: </basicHttpBinding>

Element security ma ustawiony tryb na Transport. Informuje nas to, że wykorzystane zostaną zabezpieczenia na poziomie transportu komunikatów, żądania będą pojawiały się przez połączenia SSL. Atrubuty elementu transport opisane są poniżej:

  • clientCredentialType - określa w jaki sposób będzie następowało uwierzytelnianie. Możliwe wartości to: Basic, Certificate, Digest, None, Ntlm i Windows.
  • proxyCredentialType – typ uwierzytelniania żądania za pomocą serwerów pośredniczących. Możliwe wartości: Basic, Digest, None, Ntlm, Windows.
  • realm – określa domenę, która zostanie wykorzystana, gdy uwierzytelnianie przyjmuje typ Basic, lub Digest.

Poniżej zaprezentowany został kod tworzący binding oraz konfigurujący go w sposób imperatywny:

   1: BasicHttpBinding binding = new BasicHttpBinding();
   2: binding.Name = "TransportBinding";
   3: binding.Security.Mode = BasicHttpSecurityMode.Transport;

wsHttpBinding

Binding ten wykorzystuje protokół HTTP (dokładniej HTTPS) do komunikacji. W przeciwieństwie do basicHttpBinding, serwis z którym się komunikujemy musie wspierać SOAP v1.2 oraz WS-Addressing.

Konfiguracja w pliku XML:

   1: <wsHttpBinding>
   2:   <binding name="TransportBinding">
   3:     <security mode="Transport">
   4:       <transport clientCredentialType="Basic"
   5:         proxyCredentialType="Basic"
   6:         realm="contoso" />
   7:     </security>
   8:   </binding>
   9: </wsHttpBinding>

Konfiguracja w kodzie:

   1: WSHttpBinding binding = new WSHttpBinding();
   2: binding.Name = "TransportBinding";
   3: binding.Security.Mode = SecurityMode.Transport;

netTcpBinding

Jak sama nazwa wskazuje nie wykorzystuje się tu HTTP, a TCP.

Konfiguracja w pliku XML:

   1: <netTcpBinding>
   2:   <binding name="TransportBinding">
   3:     <security mode="Transport">
   4:       <transport clientCredentialType="Windows"
   5:         protectionLevel="EncryptAndSign" />
   6:     </security>
   7:   </binding>
   8: </netTcpBinding>

Atrybut clientCredentialType określa mechanizm, który zostanie wykorzystany w celu uwierzytelnienia. Możliwe wartości:

  • Certificate – w celu uwierzytelnienia żądania dostarczany jest certyfikat,
  • None – żądania traktowane są jako anonimowe,
  • Windows – wykorzystane zostaną mechanizmy systemu Windows.

Atrybut protectionLevel pozwala określić sposób, w jaki będą szyfrowane komunikaty:

  • None – komunikat nie będzie kodowany,
  • Sign – komunikat jest podpisany cyfrowo, jednak nie jest szyfrowany,
  • EncryptAndSign – szyfrowanie oraz podpisanie w sposób cyfrowy.

To, co możemy skonfigurować w pliku XML można zrobić także przez kod:

   1: NetTcpBinding binding = new NetTcpBinding();
   2: binding.Name = "TransportBinding";
   3: binding.Security.Mode = SecurityMode.Transport;
   4: binding.Security.Transport.ClientCredentialType =
   5: TcpClientCredentialType.Windows;

netNamedPipeBinding

Nazwane potoki (ang. named pipes) są zopytmalizowane pod kątem komunikacji między procesami w ramach jednej maszyny. Implementacja związana z zabezpieczeniami jest niemal taka sama jak w przypadku netTcpBinding. Główna różnica jest taka, że TCP ma kilka dodatkowych funkcji. Poniższa konfiguracje demonstruje różnice:

   1: <netNamedPipeBinding>
   2:   <binding name="TransportBinding">
   3:     <security mode="Transport">
   4:       <transport protectionLevel="EncryptAndSign" />
   5:     </security>
   6:   </binding>
   7: </netNamedPipeBinding>

Konfiguracja przez kod:

   1: NetNamedPipeBinding binding = new NetNamedPipeBinding();
   2: binding.Name = "TransportBinding";
   3: binding.Security.Mode = NetNamedPipeSecurityMode.Transport;

msmqIntegrationBinding

Metoda zoptymalizowana pod kątem klientów w WCF, które komunikują się z serwisami MSMQ, które nie są napisane w WCF. Ten binding daje możliwość wykorzystania uwierzytelniania systemu Windows, w tym także AD DS. Wymagane jest, by klient i serwis znajdowały się w tej samej domenie. MSMQ daje możliwość dołączenia do komunikatu certyfikatu, który ma zapewnić, że komunikat został nim podpisany. Przykład:

   1: <msmqIntegrationBinding>
   2:   <binding name="TransportBinding">
   3:     <security mode="Transport">
   4:       <transport msmqAuthenticationMode="WindowsDomain"
   5:         msmqEncryptionAlgorithm="AES"
   6:         msmqProtectionLevel="EncryptAndSign"
   7:         msmqSecureHashAlgorithm="SHA1" />
   8:     </security>
   9:   </binding>
  10: </msmqIntegrationBinding>

Atrybut msmqAuthenticationMode kontroluje, w jaki sposób użytkownik zostanie uwierzytelniony. Pozostałe trzy atrybuty dotyczą zabezpieczania samego komunikatu.

Konfiguracja przez kod:

   1: MsmqIntegrationBinding binding = new MsmqIntegrationBinding();
   2: binding.Name = "TransportBinding";
   3: binding.Security.Mode = MsmqIntegrationSecurityMode.Transport;
   4: binding.Security.Transport.MsmqAuthenticationMode = MsmqAuthenticationMode.WindowsDomain;

netMsmqBinding

Wykorzystuje ten sam protokół, co msmqIntegrationBinding (MSMQ). Różnica polega na tym, że serwis docelowy będzie zaimplementowany w WCF. Konfiguracja jest praktycznie identyczna do tej w msmqIntegrationBinding:

   1: <netMsmqBinding>
   2:   <binding name="TransportBinding">
   3:     <security mode="Transport">
   4:       <transport msmqAuthenticationMode="WindowsDomain "
   5:         msmqEncryptionAlgorithm="AES"
   6:         msmqProtectionLevel="EncryptAndSign"
   7:         msmqSecureHashAlgorithm="SHA1" />
   8:     </security>
   9:   </binding>
  10: </netMsmqBinding>

Przez kod:

   1: NetMsmqBinding binding = new NetMsmqBinding();
   2: binding.Name = "TransportBinding";
   3: binding.Security.Mode = NetMsmqSecurityMode.Transport;
   4: binding.Security.Transport.MsmqAuthenticationMode = MsmqAuthenticationMode.WindowsDomain;

W następnym artykule przestawione zostaną zabezpieczenia na poziomi komunikatu.

Tagi: , , , ,

70-503: WCF Extensibility

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

We wcześniejszych wpisach omówiony został mechanizm śledzenia (ang. tracing). Co jeśli pracujemy z serwisem, do którego kodu nie mamy dostępu, a nie zostało włączone w nim śledzenie? Rozwiązaniem może być wykorzystanie potoków WCF (ang. WCF pipeline).

image

WCF jest rozszerzalny na wielu płaszczyznach. Kwestią do rozstrzygnięcia pozostaje tylko to, gdzie dodatkowa funkcjonalność powinna zostać wstrzyknięta. Rysunek po lewej stronie ilustruje ścieżkę komunikatu. Po stronie aplikacji klienta wywoływane są metody proxy. Zadaniem proxy jest utworzenie obiektu Message, który będzie zawierał parametry wywołania oraz inne informacje, jak chociażby te związane z transportem. Komunikaty te następnie trafiają na stos kanału (ang. channel stack), skąd są wysyłane do serwisu.

Po stronie serwisu strumień danych jest odbierany, trafia na stos, po czym konwertowany jest z powrotem do obiektu typu Message. Obiekt ten jest następnie przekazywany do mechanizmu dispatcher, który sprawdza parametry oraz wykonuje wywołanie odpowiedniej metody na obiekcie serwisu.

Przedstawiony schemat komunikacji dostarcza wielu miejsc, w których może zostać rozszerzony (ang. extensibility points). Rozszerzanie to podłączenie własnych klas, które zapewniają dodatkową funkcjonalność.

image

Po stronie proxy pierwszym punktem jest miejsce, gdzie parametry wysyłane do serwisu mogą zostać sprawdzone, czy zmodyfikowane jeszcze przed wysłaniem ich do serwisu. Drugi punkt to miejsce, gdzie komunikat poddawany jest serializacji. Trzeci punkt występuje po utworzeniu obiektu typu Message. Wykorzystywany jest on przykładowo do prowadzenia dziennika zdarzeń. Punkty te mogą być kontrolowane przez obiekty ClientOperation oraz ClientRuntime. Pierwszy dotyczy sprawdzania parametrów wywołania i formatowania wiadomości, drugi – dla każdej operacji.

Po stronie serwisu punkty rozszerzania są takie same, z tą różnicą, że wywoływane są w odwrotnej kolejności.

Kontroler parametrów

Jednym z głównych powodów implementowania kontrolera parametrów (ang. message inspector) jest sprawdzanie parametrów przed wysłaniem ich do serwisu, co pozwala uniknąć całego narzutu związanego z komunikacją i odrzucić błędne wywołania już po stronie klienta. Aby stworzyć kontroler należy zaimplementować interfejs IParameterInspector, który zawiera dwie metody: BeforeCall i AfterCall. Sygnatura metod przedstawiona została poniżej:

   1: void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState);
   2: object BeforeCall(string operationName, object[] inputs);

Poniżej przestawiony został kontroler, który sprawdza adres e-mail za pomocą wyrażenia regularnego. W przypadku niepoprawnych parametrów zwracany jest FaultException:

   1: public class EMailAddressInspector : IParameterInspector
   2: {
   3:   public object BeforeCall(string operationName, object[] inputs)
   4:   {
   5:     string emailAddress = inputs[0] as string;
   6:     if (!Regex.IsMatch(emailAddress, "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", RegexOptions.None))
   7:       throw new FaultException("Invalid email address format.");
   8:     return null;
   9:   }
  10:   public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
  11:   { }
  12: }

W powyższym przykładzie zaimplementowana została wyłącznie metoda BeforeCall.

Po stronie klienta metoda BeforeCall zostaja wywołana przed serializacją parametrów do obiektu Message – przed wysłaniem. Metoda AfterCall zostaje wywołana po tym, jak odpowiedź jest deserializowana – po otrzymaniu odpowiedzi z serwisu. Wstrzykiwanie klasy kontrolera po stronie klienta musi zostać wykonane poprzez kod. Dodajemy ją do obiektu OperationDescription w proxy:

   1: UpdateServiceClient proxy = new UpdateServiceClient();
   2: Proxy.Endpoint.Contract.Operations[0].Behaviors.Add(new EmailAddressInspector());

Po stronie serwera przetwarzanie przez kontroler parametrów jest bardzo podobne. Metoda BeforeCall jest wywoływana po deserializacji do obiektu platformy .NET, metoda AfterCall – po wykonaniu operacji przez serwis. Wstrzykiwanie klasy kontrolera po stronie serwisu wymaga innego rozwiązania niż po stronie klienta. Szczególnie może to być własny atrybut, implementujący interfejs IOperationBehavior:

   1: public class EmailAddressInspectorAttribute : Attribute, IOperationBehavior
   2: {
   3:     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
   4:     {
   5:         dispatchOperation.ParameterInspectors.Add(new EmailAddressInspector());
   6:     }
   7:     public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
   8:     {}
   9:     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  10:     { }
  11:     public void Validate(OperationDescription operationDescription)
  12:     { }
  13: }

Wewnątrz klasy nadpisana jest metoda ApplyDispatchBehavior, wewnatrz której dodana jest instancja kontrolera parametrów. Interfejs IOperationBehavior wymaga dodatkowo zaimplementowania jeszcze trzech metod:

  • AddBindingParameters – wykorzystywana, aby przekazać informacje podczas uruchomienia,
  • ApplyClientBehavior – modyfikuje zachowanie związane z zapytaniem po stronie klienta,
  • Validate – wykorzystywana, aby stwierdzić, czy operacja spełnia kryteria wymagane, aby zachowanie zakończyło się sukcesem

Kontroler komunikatu

Kontroler komunikatu (ang. message inspector) zostaje wywołany po sprawdzeniu parametrów i serializacji. Aby utworzyć komunikat należy wykorzystać interfejsy – IClientMessageInspector dla klienta i IDispatchMessageInspector dla serwera. Poniżej przedstawione zostały sygnatury metod:

   1: public interface IClientMessageInspector
   2: {
   3:   void AfterReceiveReply(ref Message reply, object correlationState);
   4:   object BeforeSendRequest(ref Message request, IClientChannel channel);
   5: }
   6: public interface IDispatchMessageInspector
   7: {
   8:   object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
   9:   void BeforeSendReply(ref Message reply, object correlationState);
  10: }

Wszystkie metody akceptuja obiekt typu Message jako parametr.

Klasa Message i jej cykl życia

Klasa Message wspiera strumieniowanie danych. Niesie to za sobą pewne konsekwencje. Mianowicie ciało komunikatu może zostać przetworzone wyłącznie raz podczas swojego cyklu życia, podczas drugiej próby otrzymamy InvalidOperationException. W celu sprawdzenia stanu strumienia klasa Message udostępnia właściwość MessageState, która może przyjąć pięć wartości:

  • Created – komunikat został utworzony,
  • Written – ciało komunikatu zostało zapisane,
  • Read – ciało komunikatu zostało przeczytane,
  • Copied – ciało komunikatu zostało skopiowane,
  • Closed – komunikat został zamknięty i nie można uzyskać do niego dostępu.

Ciało komunikatu może być przetwarzany wyłącznie w stanie Created.

Dodawanie kontrolera komunikatów do potoku

Możemy wybierać z trzech sposobów dodawania kontrolera po stronie klienta, czy serwisu. Utworzenie zachownia (ang. behavior) implementującego IEndpointBehavior. Wiąże się to z implementacją czterech metod: AddBindingParameters, ApplyClientBahavior, ApplyDispatchBehavior, Validate. Poniższy kod tworzy zachowanie, które następnie wstrzykuje kontroler do WCF:

   1: public class LoggingEndpointBehavior : IEndpointBehavior
   2: {
   3:     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
   4:     { }
   5:     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
   6:     {
   7:         MessageLogInspector inspector = new MessageLogInspector();
   8:         clientRuntime.MessageInspectors.Add(inspector);
   9:     }
  10:     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  11:     {
  12:         MessageLogInspector inspector = new MessageLogInspector();
  13:         endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
  14:     }
  15:     public void Validate(ServiceEndpoint endpoint)
  16:     { }
  17: }

Kiedy mamy już zachowanie należy dodać je do behavior extension. Po tym dodaniu można dodać je do potoku WCF z poziomu pliku konfuguracyjnego. Poniższy kod demonstruje tworzenie behavior extension dla klasy LoggingEndpoinBahavior, wymagane jest dziedziczenie po BehaviorExtensionsElement:

   1: public class LoggingBehaviorExtensionElement : BehaviorExtensionElement
   2: {
   3:     public override Type BehaviorType
   4:     {
   5:         get
   6:         {
   7:             return typeof(LoggingEndpointBehavior);
   8:         }
   9:     }
  10:     protected override object CreateBehavior()
  11:     {
  12:         return new LoggingEndpointBehavior();
  13:     }
  14: }

Po zaimplementowaniu zachowania, możemy je dodać poprzez element behaviorExtension w pliku konfiguracyjnym. Wpisy dodajemy w sekcji serviceModel:

   1: <system.serviceModel>
   2:   <behaviors>
   3:     <endpointBehaviors>
   4:       <behavior name="LoggingEndpointBehavior">
   5:         <messageLogger />
   6:       </behavior>
   7:     </endpointBehaviors>
   8:   </behaviors>
   9:   <extensions>
  10:     <behaviorExtensions>
  11:       <add name="messageLogger"
  12:       type="assembly.LoggingBehaviorExtensionElement, assembly,
  13: Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  14:     </behaviorExtensions>
  15:   </extensions>
  16: </system.serviceModel>

Alternatywnie zachowanie może zostać dodane w sposób imperatywny (programowy):

   1: UdpateServiceClient client = new UpdateServiceClient();
   2: client.Endpoint.Behaviors.Add(new LoggingEndpointBehavior());

W następnej lekcji przedstawione zostanie monitorowanie usług WCF.

Tagi: , , , ,