Category Archives: WCF

WCF behaviors

Zachowania w WCF umożliwiają rozszerzanie funkcjonalności poprzez np. doczepianie niestandardowych modułów. Do dyspozycji mamy 4 typy, różniące się zasięgiem obejmowania: Service, Operation, Endpoint, Contract. Utworzenie zachowania sprowadza się do implementacji odpowiedniego interfejsu, który zawiera m.in. następujące metody:

  1. AddBindingParameters – umożliwia przekazanie dodatkowych parametrów.
  2. Validate –waliduje (np. czy usługa może zostać uruchomiona).
  3. ApplyDispatchBehavior/ApplyClientBehavior – służy do dodawania rozszerzeń (np. Message Inspector).

Zacznijmy od zachowania przeznaczonego dla usługi (Service). Zachowanie musi implementować interfejs IServiceBehavior. Obejmuje w swoim zasięgu całą usługę (wszystkie endpointy). Przykład doczepiania Message Inspector’a do wszystkich endpointów:

public class ServiceBehaviour : IServiceBehavior 
{
    #region IServiceBehavior Members

    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
    }
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
        {
            foreach (EndpointDispatcher endpoint in dispatcher.Endpoints)
            {
                    endpoint.DispatchRuntime.MessageInspectors.Add(new ExampleMessageInspector());
            }
        }
    }
    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {            
    }

    #endregion
}

ServiceBehaviour może zostać doczepiony przez kod, plik konfiguracyjny lub atrybut.

Zachowania dla endpoint muszą implementować interfejs IEndpointBehavior. Zasięgiem obejmują oczywiście pojedynczy endpoint. Zachowanie można doczepić za pomocą kodu lub pliku konfiguracyjnego – brak możliwości wykorzystania atrybutu. Przykład:

public class CustomBehaviour : IEndpointBehavior
{
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {

    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        InspectorExample1 inspector = new InspectorExample1();
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }
}

Analogicznie zachowanie dla kontraktu musi implementować IContractBehavior. Zachowanie może zostać dodane wyłącznie przez atrybut lub kod. Przykład ustawiania własnej klasy zarządzającej instancjami usługi:

public class SingletonBehaviorAttribute : Attribute, IContractBehaviorAttribute, IContractBehavior
{

  #region IContractBehaviorAttribute Members

  public Type TargetContract
  {
    get { return typeof(ISampleService); }
  }

  #endregion

  #region IContractBehavior Members

  public void AddBindingParameters(ContractDescription description, ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection parameters)
  {
    return;
  }

  public void ApplyClientBehavior(ContractDescription description, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  {
    return;
  }

  public void ApplyDispatchBehavior(ContractDescription description, ServiceEndpoint endpoint, DispatchRuntime dispatch)
  {
    dispatch.InstanceProvider = new ObjectProviderBehavior("Custom ObjectProviderBehavior constructor.");
  }

  public void Validate(ContractDescription description, ServiceEndpoint endpoint)
  {
    return;
  }

  #endregion
}

Zachowania dla pojedynczych operacji muszą implementować IOperationBehavior i mogą zostać dodane za pomocą kodu lub atrybutu. Przykład dodania Parameter Inspector:

#region IOperationBehavior Members
public void AddBindingParameters(
  OperationDescription operationDescription, BindingParameterCollection bindingParameters
)
{ return; }

public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
{
  clientOperation.ParameterInspectors.Add(new Inspector());
}

public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
{
  dispatchOperation.ParameterInspectors.Add(new Inspector());
}

public void Validate(OperationDescription operationDescription){ return; }

Parameter Inspectors pełnią rolę analogiczną do Message Inspector jednak ograniczają się do metod a nie całych wiadomości SOAP np:

public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
{
  Console.WriteLine(
    "IParameterInspector.AfterCall called for {0} with return value {1}.", 
    operationName, 
    returnValue.ToString()
  );
}

public object BeforeCall(string operationName, object[] inputs)
{
  Console.WriteLine("IParameterInspector.BeforeCall called for {0}.", operationName);
  return null;
}

Wiemy już jak implementować zachowania. Pozostało wyjaśnić w jaki sposób możemy dodać zachowania tak aby dana usługa czy metoda wykorzystywała je. Zacznijmy od doczepiania różnych typów zachowań za pomocą kodu:

  1. ServiceBehaviour:
    using (ServiceHost host = new ServiceHost(typeof(Test)))
    { 
       host.Description.Behaviors.Add(customBehavour);
    }

  2. EndpointBehaviour:
    foreach (ServiceEndpoint se in host.Description.Endpoints) 
          se.Behaviors.Add(new CustomBehavior());

  3. OperationBehavour:
    foreach (ServiceEndpoint se in host.Description.Endpoints)
    {
        foreach (OperationDescription od in se.Contract.Operations)
        {
            od.Behaviors.Add(new CustomBehavior);
        }
    }

  4. ContractBehaviour:
  5. foreach (ServiceEndpoint se in host.Description.Endpoints)
    {
        se.Contract.Behaviors.Add(new CustomBehavior);
    }

Możemy również doczepić zachowanie za pomocą pliku konfiguracyjnego. W tym przypadku należy najpierw stworzyć klasę dziedziczącą po BehaviorExtensionElement:

public class CustomBehaviorExtensionElement : BehaviorExtensionElement
{
    protected override object CreateBehavior()
    {
        return new CustomBehaviour();
    }

    public override Type BehaviorType
    {
        get
        {
            return typeof(CustomBehaviour);
        }
    }
}

Musimy po prostu zwrócić instancję oraz typ zachowania. Następnie w pliku konfiguracyjnym:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <system.serviceModel>
        <services>
            <service name="WebApp.OrderService">
                <endpoint
                    behaviorConfiguration="WebAppBeaviour"
                    address="http://localhost:8000/OrderService"
                    binding="wsHttpBinding" bindingConfiguration="customWsHttpBinding"
                    contract="WebApp.OrderService.IOrderService" />
            </service>
        </services>

        <extensions>
            <behaviorExtensions>
                <add name="WebAppBeaviourElement" type="WebApp.Extensions.CustomBehaviorExtensionElement, WebApp.Extensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
            </behaviorExtensions>
        </extensions>

        <behaviors>
            <endpointBehaviors>
                <behavior name="WebAppBeaviour">
                    <WebAppBeaviourElement />
                </behavior>
            </endpointBehaviors>
        </behaviors>

        <bindings>
            <wsHttpBinding>
                <binding name="customWsHttpBinding">
                    <security mode="None" />
                </binding>
            </wsHttpBinding>
        </bindings>

    </system.serviceModel>

</configuration> 

Jak widać w pliku konfiguracyjnym, klasa BehaviorExtensionElement  stanowi mediator pomiędzy plikiem XML a kodem konkretnym zachowaniem.

Ostatnią opcją jest użycie atrybutów .W tym przypadku należy oprócz implementacji stosownego zachowania (np. IServiceOperation) również dziedziczyć po Attribute np:

public class CustomBehaviour : Attribute,IServiceBehavior
{
    //...
}

[CustomBehaviour(MaxPoolSize=10,MinPoolSize=2,IncrementSize=2)]
public class Service:IService
{
    //...
}

WCF Message Inspector–wdrożenie (część II)

W poprzednim poście pokazałem jak stworzyć prosty Message Inspector, wyświetlający nagłówek lub wiadomość SOAP. Dzisiaj zajmiemy się podłączeniem zaimplementowanej klasy do usługi. Najładniejszym moim zdaniem rozwiązaniem jest implementacja behaviour’a, który następnie dołączy zaimplementowany message inspector. W przyszłości planuje napisać osobny post o behaviorach i ich zastosowaniu w WCF. Z punktu widzenia message inspector wystarczy wiedzieć, że behaviory służą do rozszerzania funkcjonalności np. endpoint’a.

Zaczynamy więc od implementacji behaviour’a dla endpoint’a:

public class CustomBehaviour : IEndpointBehavior
{
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {

    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        InspectorExample1 inspector = new InspectorExample1();
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }
}

ApplyDispatchBehavior wykorzystywany jest dla zachowań usługi (nie klienta). W metodzie po prostu tworzymy inspector i dodajemy do kolekcji. Aby skonfigurować w web.config endpoint zgodnie z behaviorem musimy stworzyć jeszcze BehaviorExtensionElement:

public class CustomBehaviorExtensionElement : BehaviorExtensionElement
{
    protected override object CreateBehavior()
    {
        return new CustomBehaviour();
    }

    public override Type BehaviorType
    {
        get
        {
            return typeof(CustomBehaviour);
        }
    }
}

Jak widać wystarczy przeładować metodę i właściwość. Klasa jest niezbędna aby wykorzystać wcześniej zdefiniowane zachowanie w pliku web.config. Pozostało nam już tylko zmodyfikować odpowiednio plik konfiguracyjny:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <system.serviceModel>
        <services>
            <service name="WebApp.OrderService">
                <endpoint
                    behaviorConfiguration="WebAppBeaviour"
                    address="http://localhost:8000/OrderService"
                    binding="wsHttpBinding" bindingConfiguration="customWsHttpBinding"
                    contract="WebApp.OrderService.IOrderService" />
            </service>
        </services>

        <extensions>
            <behaviorExtensions>
                <add name="WebAppBeaviourElement" type="WebApp.Extensions.CustomBehaviorExtensionElement, WebApp.Extensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
            </behaviorExtensions>
        </extensions>

        <behaviors>
            <endpointBehaviors>
                <behavior name="WebAppBeaviour">
                    <WebAppBeaviourElement />
                </behavior>
            </endpointBehaviors>
        </behaviors>

        <bindings>
            <wsHttpBinding>
                <binding name="customWsHttpBinding">
                    <security mode="None" />
                </binding>
            </wsHttpBinding>
        </bindings>

    </system.serviceModel>

</configuration> 

BehaviourExtension umożliwił wykorzystanie zachowania w pliku konfiguracyjnym. Bezpośrednio do behavior’a nie możemy odwoływać się w web.config.

WCF Message Inspector–implementacja (część I)

Używając Message Inspector możemy modyfikować wiadomości przychodzące i wychodzące zarówno po stronie serwera jak i klienta. Wystarczy zaimplementować dwa interfejsy: IClientMessageInspector oraz IDispatchMessageInspector. Ich definicja wygląda następująco:

public interface IClientMessageInspector
{
    void AfterReceiveReply(ref Message reply, object correlationState);
    object BeforeSendRequest(ref Message request, IClientChannel channel);
}
public interface IDispatchMessageInspector
{
    object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
    void BeforeSendReply(ref Message reply, object correlationState);
}

Pierwszy z nich służy do tworzenia filtrów po stronie klienta a drugi po stronie usługi. Każda z metod wywoływana jest w momencie odebrania odpowiedzi lub wysłania zapytania. Warto zwrócić uwagę, że wiadomości przekazywane są przez referencję(REF). Oznacza to, że możemy tworzyć całkowicie nowy obiekt (wiadomość) przez operator new.

Wartość zwrócona w BeforeSendRequest jest potem przekazywana w parametrze correlationState AfterReceiveReply. Spróbujmy napisać prosty filtr, który wyświetli wiadomość w momencie wysyłki i odbioru:

public class SampleMessageInspector : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
        request = buffer.CreateMessage();
        Console.WriteLine(buffer.CreateMessage().ToString());
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
        reply = buffer.CreateMessage();
        Console.WriteLine(buffer.CreateMessage().ToString());
    }
}

Innym przykładem jest wyświetlenie wszystkich nagłówków SOAP:

public class MessageInspectorExample2 : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
        request = buffer.CreateMessage();
        Message originalMessage = buffer.CreateMessage();
        foreach (MessageHeader header in originalMessage.Headers)
        {
            Console.WriteLine("\n{0}\n", header);
        }
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
        reply = buffer.CreateMessage();
        Message originalMessage = buffer.CreateMessage();
        foreach (MessageHeader header in originalMessage.Headers)
        {
            Console.WriteLine("\n{0}\n", header);
        }
    }
}

Analogicznie wygląda sytuacja w przypadku klienta. W prawdziwym przypadku oczywiście w metodach umieścilibyśmy bardziej zaawansowaną logikę (np. walidacja luz wykonywanie logów). W następnej części przedstawię w jaki sposób można podłączyć Message Inspector do klienta lub usługi.

WCF Data Services – aplikacja kliencka

W poprzednim poście przedstawiłem w skrócie czym jest WCF Data Service i jak stworzyć prostą usługę sieciową opartą o WCF Data Service. Dzisiaj przyjrzymy się jak stworzyć aplikacje kliencką za pomocą bibliotek dołączonych do .NET.

.NET ułatwia znacząco dostęp do usług WCF Data Service. Programista nie musi samemu tworzyć zapytań URL. Korzystanie z WCF Data Service bardzo przypomina pracę z lokalnym EntityFramework. Stwórzmy więc aplikację kliencką:

  1. Tworzymy nowy projekt aplikacji klienckiej np. WPF lub WindowsForms.
  2. Dodajemy referencję do usługi za pomocą Add Service Reference (menu kontekstowe).
  3. W zasadzie już wszystko jest gotowe! Teraz wystarczy tylko wywoływać odpowiednie metody w celu np. selekcji danych.

Jak już wspomniałem korzystanie z WCF Data Service przypomina prace ze zwykłym, lokalnym EntityFramework. Należy jednak pamiętać, że WCF Data Service to zwykła usługa sieciowa. Utworzenie instancji klienta usługi WCF Data Service różni się tym od zwykłego WCF, że należy przekazać adres usługi w konstruktorze:

Sales.DataServicesEntities context = new new Sales.DataServicesEntities(serviceUri);

W przypadku inicjalizowania klienta zwykłej usługi wystarczyłby wyłącznie konstruktor bezparametrowy. Spróbujmy zatem zwrócić listę wszystkich encji Product:

dataGrid1.ItemsSource = m_Context.Products;

W celu znalezienia encji o wskazanym ID wystarczy:

this.m_Context.Products.Where((product) => product.BarCode == productSearch.BarCode)

Sortowanie danych:

m_Context.Products.OrderBy((product) => product.BasePrice);

Wstawianie encji:

m_DataContext.AddToProducts(ProductModel);

Usuwanie encji:

m_DataContext.DeleteObject(ProductModel);

Aktualizacja obiektu:

m_Context.UpdateObject(Product)

Warto jeszcze raz podkreślić, że WCF Data Service korzysta z protokołu HTTP. Dzięki temu bardzo łatwo wprowadzić buforowanie (HTTP caching).

Jak widać, z WCF Data Service można korzystać za pomocą języka LINQ. Niestety niektóre konstrukcje nie są wspierane. Poniższe zapytanie LINQ (join) nie zostanie prawidłowo przetłumaczone na URL:

            var invoices = from orderItem in m_Context.OrderItems
                    join invoice in m_Context.Invoices on
                        orderItem.ID_INVOICE equals invoice.ID_INVOICE
                    select invoice;

Jedynym sposobem na złączenie encji jest wykorzystanie metody expand oraz zdefiniowanych na serwerze relacji:

IEnumerable<Sales.Invoice> invoices = m_Context.Invoices.Expand("OrderItems");

Należy podkreślić, że expand jest bardzo niewydajne ponieważ złączenie występuje lokalnie w pamięci a nie w silniku baz danych. W przyszłości postaram się zamieścić cały artykuł o WCF Data Service – to co w tych dwóch postach przedstawiłem to tylko próbka możliwości WCF Data Service:).

WCF Data Services

WCF Data Service to usługa sieciowa umożliwiająca łatwy dostęp do danych. Wyobraźmy sobie następujący przypadek:

image

Mamy pewną bazę danych zawierającą np. informacje o produktach. Można napisać ręcznie usługę WCF, która wyeksponuje wszelkie potrzebne dane za pomocą metod. Usługa w takim przypadku zawierałaby metody typu Create, Update, Delete, GetById, GetByQuery itp. Implementacja usługi dla każdej tabeli w bazie jest dość czasochłonna i niezbyt interesująca. Za pomocą WCF Data Service, usługa zostanie stworzona automatycznie na podstawie modelu encji (np. Entity Framework). Ponadto komunikacja między klientem a WCF może odbywać się za pomocą zapytań LINQ, tak jakbyśmy korzystali z lokalnego Entity Framework.

WCF Data Service oparty jest o architekturę REST (tzw. RESTful Service). Architektura REST składa się z serwera oraz klienta. Klient wysyła żądanie do serwera, które następnie jest przetwarzane. Serwer ma dostęp do zasobów i zwraca tzw. reprezentację zasobów (representation of resource), która przeważnie przyjmuje postać dokumentu XML. Zasoby muszą mieć jakiś jednoznaczny identyfikator (np. URI). Serwer nie przechowuje informacji o stanie – to klient odpowiedzialny jest za przejścia między różnymi stanami.

Architektura REST musi spełniać następujące warunki:

•    Bezstanowość ,
•    Buforowanie danych,
•    SoC  (separation of concerns),
•    Budowa warstwowa,
•    Ujednolicony protokół komunikacji.

Sama idea architektury jest stara (po raz pierwszy została przestawiona w  pracy doktorskiej Roya T. Fieldinga, 2000). W praktyce protokołem komunikacji między serwerem a klientem jest najczęściej HTTP. W takim przypadku identyfikatorem zasobów jest zwykły URL (http://www.localhost/users). W zależności od użytej metody HTTP, serwer wykona jedną z następujących operacji:

  1. HTTP POST – tworzenie zasobu,
  2. HTTP GET – selekcja danych ,
  3. HTTP PUT – aktualizacja,
  4. HTTP DELETE –  usunięcie danych.

Przykładowo, poniższe zapytanie spowoduje pobranie listy użytkowników z bazy danych:

GET /users HTTP/1.1
Host: localhost
Accept: application/xml

Usługa sieciowa, po otrzymaniu takiego zapytania wywoła metodę odpowiedzialną za selekcję danych z kolekcji encji users. Architektura WCF Data Service wygląda następująco (źródło MSDN):

image

Klient komunikuje się z usługą za pomocą HTTP. Następnie Data Services Runtime wykorzystując np. Entity Framework pobiera dane z bazy. Możliwe jest napisanie własnych provider’ów, które mogą nawet odwoływać się do baz nierelacyjnych.

Myślę, że teorii na początek wystarczy. Więcej informacji o REST znajdziecie np. na wiki :). Skupmy się teraz jak to wygląda od strony praktycznej. Większość operacji wykona za nas IDE:

  1. Na początek należy stworzyć oczywiście projekt WCF Service Application.
  2. Po utworzeniu nowego projektu, dojemy model encji ADO .NET Entity Data Model (Add -> New Item -> Data ->ADO .NET Entity Data Model). Kreator poprowadzi nas i np. wygeneruje model encji na podstawie bazy danych .
  3. Następnie dodajemy Data Service (Add -> New Item -> Web -> WCF Data Service).
  4. Zostanie wygenerowany szablon usługi. Należy teraz wstawić w miejsce komentarza prawidłowy model encji. Po zmianach kod powinien wyglądać np. następująco:
    public class SalesSystemService : DataService<DataServicesEntities>
    {
    }

  5. WCF Data Service pozwala zdefiniować prawa dostępu. Można umożliwić klientom np. dostęp tylko do odczytu do produktów:

    public static void InitializeService(DataServiceConfiguration config)
    {                                    
        config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead);            
    }
    

W zasadzie więcej konfiguracji nie trzeba! Wystarczyło stworzyć model encji, DataService, podmienić typ generyczny oraz skonfigurować prawa dostępu. W tej chwili możemy odpalić usługę i za pomocą przeglądarki przetestować jej działanie.

Aby uzyskać listę wszystkich produktów wystarczy w przeglądarce wpisać http://localhost:5744/Services/SalesSystemService.svc/Products. Jeśli chcemy aby został zwrócony tylko pojedynczy produkt o wskazanym kluczu głównym wystarczy wpisać http://localhost:5744/Services/SalesSystemService.svc/Products(4). Do dyspozycji jest naprawdę wiele sposobów selekcji. Przykładowo http://localhost:5744/Services/SalesSystemService.svc/Products?$filter=BarCode eq ’55’  zwróci produkty tylko o kodzie kreskowym równym 55.

Wszystkie możliwe zapytania są tematem na osobny post. Microsoft dostarcza biblioteki ułatwiające komunikacje z WCF Data Service. Użytkownik (programista) nie musi ręcznie pisać adresów URL. Może skorzystać z API oraz języka LINQ. Jak to wygląda po stronie klienta przedstawię już w następnym poście.