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).
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ść.
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.