sobota, 12 października 2013

Czy .NET ma przyszłość?

Z pewnością wielu zadaje sobie to pytanie podchodząc do nauki nowej platformy.
Języka programowania można nauczyć się bardzo szybko - znając jeden opanowanie kolejnego zajmie maksymalnie dzień.
Nauka platformy i frameworków może zająć miesiące a nawet dłużej. Frameworki ewoluują, dostarczają nowe lepsze rozwiązania często pojawiających się problemów. Nauka i zmiana przyzwyczajeń związana jest z każdą nową wersją.
Naturalnie pojawia się więc pytanie czy warto jest inwestować swój czas w daną platformę, tutaj .NET. Świetnie na to pytanie odpowiada Rockford Lhotka. Zachęcam do przeczytania jego postu na ten temat - http://www.lhotka.net/weblog/DoesNETHaveAFuture.aspx.

poniedziałek, 30 września 2013

nHibernate logowanie operacji

Bardzo często chcemy wiedzieć co dzieje się w naszej aplikacji. Podstawową formą śledzenia aplikacji jest logowanie informacji do pliku/bazy lub innego nośnika z którego można następnie je odtworzyć i analizować.

nHibernate świetnie w tej materii współpracuje z biblioteką log4net. Bibliotece tej poświęcę osobny post - w skrócie umożliwia ona tworzenie logów. Konfiguracja lo4net możne zostać zapisana w pliku konfiguracyjnym aplikacji jak i bezpośrednio w kodzie.

Aby logować to co dzieje się w nHibernate należy dodać do solucji biblioteki. Można tego dokonać z poziomu NuGet:


Dzięki użyciu NuGet log4net zostanie wstępnie skonfigurowany w app.config. Po uruchomieniu aplikacji korzystającej z nHibernate, na ekranie konsoli zostaną wypisane informacje z aktualnie wykonywanych czynności w nHibernate:


Dodatkowo wchodząc w ustawienia session factory możemy dodać parametr show_sql. Parametr ten doda w trybie logowania na konsolę dane wartości przekazywanych parametrów do zapytania SQL:

Code:
  <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
    <session-factory>
      <property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="connection.connection_string">Server=localhost;initial catalog=AdventureWorksLT2012;Integrated Security=True</property>
      <property name="show_sql">true</property>
      <mapping assembly="AdventureWorksSampleHibernateLogging" />
    </session-factory


W tym poście przedstawiłem w jaki sposób logować na konsolę. Nic nie stoi na przeszkodzie aby te dane logowane były do bazy czy pliku. Wybór należy do nas.

Plik z solucją: http://sdrv.ms/17kyth8

niedziela, 29 września 2013

Generowanie kluczy głównych w nHibernate

Każda tabela w bazie danych powinna posiadać kolumnę zwaną kluczem głównym. Dzięki tej kolumnie jesteśmy w stanie jednoznacznie odróżnić wiersze w bazie danych.
nHibernate posiada kilka generatorów kluczy głównych. Możemy je podzielić na dwie kategorie:
  • takie, które wymagają odpytania bazy danych po wstawieniu rekordu do bazy o wygenerowany id
  • takie, które wstawiają id wraz z wierszem
Pierwszy sposób jest niekorzystny, gdyż aby pobrać wartość id klucza należy wywołać dodatkowe zapytanie do bazy danych - zwiększamy obciążenie na bazie danych, jednocześnie zmniejszając wydajność aplikacji.

Pierwsza grupa zawiera następujące generatory:
  • identity -zwraca id wygenerowane przez bazę
  • select - używa zapytania SELECT aby pobrać id rekordu (potrzebny jest w takim wypadku klucz naturalny)
  • sequence-identity - używany dla baz obsługujących sekwencje
  • trigger-identity - zwraca id wygenerowane przez trigger
  • native - w zależności od bazy danych (np. dla MS SQL identity, dla Oracle - sequence-identity)
Druga grupa jest szersza i zawiera więcej możliwości:
  • hilo - użyty jest algorytm Hi/Lo - rezerwowany jest zakres identyfikatorów numerycznych. Po skończeniu danego zakresu rezerwowany jest kolejny. Jest to bezpieczne rozwiązanie, gdyż użyta jest w bazie dodatkowa tabela przechowująca rezerwowane zakresy
  • guid - wykorzystuje GUID
  • guid.comb - łączy 10 bajtowy GUID wraz z 6 bajtową datą. Zmniejsza to poziom fragmentacji indeksu
  • guid.native - pobiera GUID z bazy (wymaga to odwołania do bazy dnaych)
  • uuid.hex - tworzy GUID numeryczny
  • uuid.string - zapisuje GUID w postaci binarnej - w takiej postaci jest nieczytelny dla człowieka 
  • counter - licznik inicjalizowany z zegara systemowego; nie można go stosować w rozwiązaniach gdzie baza jest współdzielona 
  • increment - rozwiązanie podobne do poprzedniego - wartość początkowa licznika pobierana jest na starcie jako MAX(Id) i inkrementowana w czasie działania programu. Nie nadaje się w rozwiązaniach gdzie jest współdzielona baza danych.
  • sequence - korzysta z sekwencji dostępnych w takich bazach jak Oracle i PostgreSql
  • seqhilo - łączy algorytm Hi/Lo z sekwencjami w celu poprawienia wydajności
  • foreign - używa klucza z innej encji - najczęściej wykorzystywany w przypadku relacji one-to-one
Każdy sposób ma swoje wady i zalety. Najbezpieczniejszą metodą jest GUID, który ze względu na swoją długość zapewnia unikalność pomiędzy wieloma instancjami aplikacji korzystającymi z tej samej bazy danych.
Jeżeli budujemy prostą aplikację, a z bazy będzie korzystała tylko jedna aplikacja możemy spokojnie jako klucza głównego użyć typu całkowitoliczbowego (integer).

nHibernate " is not mapped"

Wyjątek z tematu "[Class] is not mapped" jest jednym z częściej występujących błędów podczas pracy z nHibernate. Związane jest to głównie z tym, że większość pracy wykonujemy manualnie wprowadzając nazwy klas i mapowań jako zwykłe ciągi znaków (string) co przekłada się na ilość błędów.

Przyczyn tego wyjątku można szukać w kilku miejscach. Miejsca w których można popełnić błędy podzieliłem na 4 obszary:
  1. App/web config
  2. Plik mapowania klasy na tabelę w bazie
  3. Tworzenie sesji
  4. Zapytania


1. App/web .config
W pliku tym ustawiamy podstawowe właściwości fabryki sesji. Jednym z ważniejszych parametrów jest ten, mówiący gdzie znajdują się mapowania klas do tabel:


Code:
  <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
    <session-factory>
      <property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="connection.connection_string">Server=localhost;initial catalog=AdventureWorksLT2012;Integrated Security=True</property>
      <mapping assembly="nHibernateCreatingMapping" />
    </session-factory>
  </hibernate-configuration>

Po wystąpieniu tego błędu sprawdzamy więc w/w plik i szukamy czy znajduje się tam linijka mówiąca w którym assembly znajdują się mapowania.


2. Plik mapowania klasy na tabelę w bazie
W tym pliku można popełnić szereg błędów:


Code:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="nHibernateCreatingMapping" namespace="nHibernateCreatingMapping.Domain">
  <class name="ProductDescription" table="SalesLT.ProductDescription">

Rzeczy które sprawdzamy:
  • czy nazwa assembly jest na pewno poprawna
  • czy namespace się zgadza
  • czy w nazwie klasy nie popełniliśmy błędu
  • czy we właściwościach pliku zaznaczyliśmy Build Action na Embedded Resource
Jak widać możliwość popełnienia 4 błędów - całkiem sporo.


3. Tworzenie sesji:


Code:
            ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
            var session = sessionFactory.OpenSession();

Tutaj także można popełnić niewinny błąd i przez pośpiech możemy zapomnieć o wywołaniu metody Configure przed stworzeniem obiektu fabryki sesji.


4. Zapytania
Jeżeli do tworzenia zapytań wykorzystujemy HQL, należy pamiętać że w zapytaniach używamy nazwy klasy a nie tak jak by się mogło wydawać - tabeli.


Code:
var productDescriptions = session.CreateQuery("from ProductDescription").List<ProductDescription>();



Wiele osób porzuca nHibernate z powodu problemów konfiguracyjnych. Po kilku dniach używania biblioteki można w łatwy sposób identyfikować miejsca gdzie potencjalnie popełniliśmy błąd. Aby ułatwić sobie pracę i przy okazji używać silnie typowanych danych można skorzystać z fluent nHibernate - ale o tym w osobnym poście.

sobota, 28 września 2013

nHibernate - początki

Do ORM nHibernate podchodziłem już kilkakrotnie. Może wynikało to ze względu na to, że całą konfigurację wykonywało się w XML, a może to że nie wspierał w pełni LINQ.
Od czasów kiedy chciałem się nim zająć wiele się zmieniło w tej kwestii i obecnie wersja 3.3 zapewnia bardzo wiele z tego, co kiedyś wydawało się niemożliwe.
Parę faktów:
Aby rozpocząć korzystanie z biblioteki należy pobrać albo z sourceforge spakowane binarki, albo za pomocą NuGet-a ściągnąć pliki do projektu:



Po dodaniu referencji do biblioteki warto dodać pliki schemy, aby ułatwić sobie pisanie mappingów. W tym celu dodajemy do solucji folder Schema i wrzucamy do niego pliki schemy udostępnione wraz z nHibernate:


Aby pliki mapowania mogły działać i podpowiadać składnię w Visual Studio, należy je dodać do obsługiwanych plików schemy. Aby to zrobić wchodzimy w którykolwiek plik ze schemą, w meni XML wybieramy opcję XML->Schemas... i wskazujemy miejsce gdzie znajdują się pliki:


Teraz możemy napisać pierwsze mapowanie. W celach pokazowych posłużę się bazą MS SQL 2012 AdventureWorksLT2012. W bazie tej zawarta jest tabela ProductDescription do której stworzę przykładowe mapowanie. 

Najpierw stworzymy klasę która odpowiada tabeli w bazie danych:



Code:
using System;

namespace nHibernateCreatingMapping.Domain
{
 public class ProductDescription
 {
  public virtual int ProductDescriptionID { get; protected set; }
  public virtual string Description { get; set; }
  public virtual Guid Rowguid { get; protected set; }
  public virtual DateTime ModifiedDate { get; set; }
 }
}

Teraz stworzymy plik mapujący klasę do tabeli w bazie danych:


Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="nHibernateCreatingMapping" namespace="nHibernateCreatingMapping.Domain">
  <class name="ProductDescription" table="SalesLT.ProductDescription">
    <id name="ProductDescriptionID">
      <generator class="identity" />
    </id>
    <property name="Description" />
    <property name="Rowguid" />
    <property name="ModifiedDate" />
  </class>
</hibernate-mapping>


Bardzo ważne: w właściwościach pliku z mapowaniem należy ustawić parametr Build Action na Embedded Resource (o innych możliwościach napiszę w osobnym poście).

Kolejną czynnością będzie dodanie konfiguracji do app.config w celu ustawienia podstawowych opcji nHibernate:


Code:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
  </configSections>
  <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
    <session-factory>
      <property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="connection.connection_string">Server=localhost;initial catalog=AdventureWorksLT2012;Integrated Security=True</property>
      <mapping assembly="nHibernateCreatingMapping" />
    </session-factory>
  </hibernate-configuration>
  <connectionStrings>
    <add name="AdventureWorks" connectionString="Data Source=localhost;Integrated Security=true;Initial Catalog=AdventureWorksLT2012" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>    
</configuration>


Po ustawieniu wszystkich parametrów możemy przystąpić do tworzenia sesji nHibernate i odpytywania bazy danych:


Code:
            ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
            var session = sessionFactory.OpenSession();
            var productDescriptions =
                session.Query<ProductDescription>()
                    .Where(x => x.ProductDescriptionID > 10 && x.ProductDescriptionID < 100);
            foreach (var productDescription in productDescriptions)
            {
                Console.WriteLine("{0} {1} {2}", productDescription.ProductDescriptionID,
                    productDescription.ModifiedDate, productDescription.Description);
            }

Rezultat:



W kolejnych opiszę dokładniej mechanizmy rządzące nHibernate, bardziej zaawansowane przypadki mapowania i inne ciekawe aspekty tej biblioteki.

niedziela, 15 września 2013

Spring.net DI?

Do tej pory w większości projektów używałem jednego z dwóch kontenerów DI: Unity lub Ninject. Kontenery te są wygodne i pozwalają na dużą swobodę konfiguracji. Nowa praca, nowe wyzwania - nowy kontener - Spring.net. Jak się sprawdza w codziennej pracy w porównaniu do poprzednich?

Najpierw instalacja - tutaj z pomocą przychodzi NuGet - w wyszukiwarce wpisujemy słowo kluczowe "spring". Pierwszy wynik czyli Spring.Core to interesujący nas składnik:






Referencje dodane czas na działanie. Aby coś testować, należy przygotować pewnego rodzaju grunt. Do testów tworzymy prosty interfejs:


Code:
    public interface ICalculator
    {
        int Add(int a, int b);
        int Subtract(int a, int b);
    }

oraz klasę implementującą:


Code:
    class Calculator : ICalculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }

        public int Subtract(int a, int b)
        {
            return a - b;
        }
    }


Pierwsza zmiana w stosunku do poprzednich kontenerów DI jest taka, iż Spring.net pozwala na konfigurację wyłącznie w XML (według dokumentacji ma zostać dodana w przyszłości możliwość konfiguracji za pomocą atrybutów). Jest to z jednej strony i zaleta i wada. W mojej ocenie brak możliwości mapowania w kodzie czy też za pomocą atrybutów delikatnie skreśla tę bibliotekę z listy współczesnych frameworków DI.
Warto w tym momencie pobrać xsd springa w celu wsparcia IntelliSense. Z menu XML wybieramy opcję Schemas... i wskazujemy gdzie znajduje się plik xsd:


Dzięki temu posunięciu tworzenie xml-a będzie trochę prostsze i popełnimy mniej błędów.
Definicje obiektów możemy przechowywać w pliku XML app.config lub dowolnym innym. Przechowanie ich w app.config jest chyba najwygodniejsze - wszystkie ustawienia trzymamy w jednym pliku a nie rozrzucamy po kilku. Najłatwiejszym sposobem jest dodanie sekcji konfiguracyjnej spring i następnie odczytanie jej w aplikacji. Przykładowa rejestracja sekcji:


Code:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
  </configSections>

  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>

  <spring>
    <context>
      <resource uri="config://spring/objects"/>
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="calculator" type="SpringNetSample.Calculator, SpringNetSample" />
    </objects>
  </spring>
</configuration>


Samo pobranie w kodzie obiektu przebiega w następujący sposób:

Code:
            IApplicationContext applicationContext = ContextRegistry.GetContext();
            ICalculator calculator = (Calculator) applicationContext.GetObject("calculator");
            Console.WriteLine(calculator.Add(1,2));


Podsumowując prosty przykład obrazujący jak użyć Spring.net. Niestety brak możliwości konfiguracji za pomocą kodu/atrybutów nie pozwala go uznać za nowoczesny kontener DI. Z drugiej strony, zmuszając programistę do używania XML ułatwia późniejszą konfiguracje administratorom systemu.
Kolejnym minusem jest brak metod generycznych pozwalających pobrać typ który chcemy dostać - można to oczywiście obejść za pomocą metod rozszerzających.

Podsumowując - spełnia swoją rolę jako kontener DI, jednak technologicznie pozostał w epoce .NET 2.0

Nie testuję tutaj wydajności kontenera. Moim zdaniem nie ważne czy kontener tworzy 1000 obiektów na sekundę a drugi 1050, gdyż ważniejsze są, moim zdaniem, możliwości i wygoda korzystania z danego kontenera. Szybkość w przypadku użycia kontenerów DI nie ma dużego znaczenia.

Problem z instalacją Update 3 Visual Studio 2012 - KB2707250

Korzystając z wolnej chwili chciałem uaktualnić VS 2012 SP2 do wersji SP3.
Niestety w momencie uruchomienia instalatora zostało wyświetlone okienko z informacją o błędzie:

Visual Studio 2012 Update 3 (KB2707250) has stopped working. A problem caused the program to stop working correctly. Please close the program.
No cóż, błąd zdarza się. Pomyślałem, że pewnie przez ilość dodatków do VS, moja instalacja po prostu się popsuła i trzeba ją naprawić. Po wejściu do Panelu Sterowania i naciśnięciu klawisza Change otrzymałem ten sam błąd, który jest przedstawiony powyżej.
Czyli jednym słowem nie mogłem ani zainstalować ani tym bardziej odinstalować VS :)
Ponieważ o błędzie nie znalazłem za dużo informacji w sieci, a sama treść też nie mówi za dużo, postanowiłem poszukać innego sposobu.
Na msdn znalazłem artykuł, opisujący dostępne przełączniki podczas instalowania VS.
Bingo! Jeden z nich:
/Log Filename Specifies a location for the log file
Pozwala na logowanie informacji podczas instalacji VS. Nie czekając dłużej odpaliłem konsolę i poczekałem na utworzenie pliku z logiem. Dzięki zalogowaniu informacji odnalazłem przyczynę moich problemów:






Po zobaczeniu tego wpisu, przypomniałem sobie, że kiedyś modyfikowałem machine.config. Wszedłem pod wskazany folder, przywróciłem oryginalny plik. Instalator się uruchomił i byłem w stanie zainstalować poprawki :)

Morał jest taki, że warto w erze myszkologi pamiętać, że istnieje stara dobra konsola :)

Hostowanie serwisu REST, przetwarzanie wiadomości, tworzenie klientów

Aby ułatwić hostowanie serwisów opartych o REST została przedstawiona klasa WebServiceHost. Klasa ta zawiera potrzebne bindingi.
Jedyne co musimy podać to typ serwisu oraz adres pod którym będzie dostępny:


Code:
            var webServiceHost = new WebServiceHost(typeof(Service1), new Uri(@"http://localhost/RestService/"));
            webServiceHost.Open();
            Console.WriteLine("Press key to close");
            Console.Read();
            webServiceHost.Close();


Aby napisać klienta serwisu REST można skorzystać z klasy WebChannelFactory<T>. Kod tworzenia jest wręcz identyczny jak w przypadku połączenia z serwisem SOAP:


Code:
            var webChannelFactory = new WebChannelFactory<IService1>(new Uri("http://localhost/RestService/"));
            var channel = webChannelFactory.CreateChannel();
            List<Person> persons = channel.GetPersons();
            foreach (var person in persons)
            {
                Console.WriteLine("{0} {1}", person.Id, person.Name);
            }



Warto jeszcze w tym miejscu wspomnieć o klasie WebOperationContext. Klasa ta umożliwia dostęp do aktualnie przetwarzanego requestu w metodach serwisu. Możemy dzięki temu pobrać następujące dane:
  • URI wywołania
  • nagłówki 
  • ETag
  • format danych
  • długość wiadomości
Przykładowe wywołanie podczas wykonywania metody serwisu:


Czym jest REST i jak go użyć w WCF

Pisząc o WCF nie sposób pominąć REST. REST (Representational state transfer) - jest wzorcem pisania aplikacji rozproszonych udostępnianych po HTTP. REST zapewnia bardzo dużą skalowalność przez to iż nie jest przechowywany stan pomiędzy żądaniami.
Opiera się o słowa kluczowe dobrze znane z protokołu HTTP: GET, POST, PUT, DELETE. Dane mogą być przesyłane w postaci HTML, XML, JSON.
Wykorzystanie REST jest bardzo szerokie. Wystarczy wymienić, że tacy potentaci jak google, facebook czy flickr udostępniają cześć swojej funkcjonalności właśnie za pomocą REST.

WCF pozwala w bardzo prosty sposób udostępnić serwis jako REST.
Najważniejsza jest poprawna konfiguracja serwisu:


Code:
<?xml version="1.0"?>
<configuration>

  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5"/>
  </system.web>
  <system.serviceModel>
    <services>
      <service name="RestSample.Service1">
        <endpoint address="" binding="webHttpBinding" contract="RestSample.IService1" behaviorConfiguration="sampleBehavior">
        </endpoint>
      </service>
    </services>

    <behaviors>
      <endpointBehaviors>
        <behavior name="sampleBehavior">
          <webHttp helpEnabled="true"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <!--
        To browse web app root directory during debugging, set the value below to true.
        Set to false before deployment to avoid disclosing web app folder information.
      -->
    <directoryBrowse enabled="true"/>
  </system.webServer>

</configuration>


Dwie najważniejsze rzeczy jakie musimy wprowadzić do naszego standardowego configu:
  • typ bindingu - webHttpBinding 
  • do behaviors dodajemy webHttp
Co oznacza webHttpBinding i webHttp w przypadku tej konfiguracji?
WebHttpBinding:
  • tworzy kanał Http
  • wprowadza wsparcie dla plików cookie
  • protokoły i bezpieczeństwo (HTTP, HTTPS)
  • kodowanie wiadomości: XML, JSON
WebHttpBehavior:
  • operacje selekcji danych
  • serializacja danych
  • obsługa wywołań

Metody interfejsu możemy oznaczyć dodatkowymi atrybutami:
WebGet:
  • odpowiada czasownikowi HTTP GET
  • pozwala ustawić adres wywołania operacji (UtlTemplate) oraz parametry przekazywane do operacji
  • format wiadomości wyjściowej (XML, JSON)
 WebInvoke
  • pozostałe czasowniki protokołu HTTP
  • domyślnie wywoływany jest POST
  • pozwala na przekazanie parametrów; ostatni parametr wskazuje gdzie ma zostać zdeserializowana zawartość wiadomości
Warto zauważyć jeszcze jeden aspekt przedstawionej konfiguracji - helpEnabled. Dzięki temu przełącznikowi serwis jest w stanie sam wygenerować dokumentację wraz z przykładowymi zapytaniami. Przykładowa konfiguracja wygląda w następujący sposób:




Jest to bardzo przydatne - nie musimy tworzyć własnej dokumentacji - zostaje ona wygenerowana automatycznie.

70-513 Udostępnianie serwisu WCF

Aby ktoś mógł skorzystać, ze stworzonego przez na serwisu należy go udostępnić.

Adres
Adres wskazuje gdzie znajduje się nasz serwis składa się z 4 składowych: schema, nazwa hosta, portu, ścieżki (przeważnie nazwa serwisu). Przykład: http://www.contoso.com/OrderService/

Binding
Binding definiuje sposób, w jaki będzie można się dostać do serwisu. Są to elementy takie jak protokół, SSL. WCF udostępnia domyślnie kilka predefiniowanych możliwości komunikacji:
  • basicHttpBinding - HTTP, HTTPS, MTOM
  • wsHttpBinding - HTTP, HTTPS, MTOM - oferuje niezawodność, tranzakcyjność, bezpieczeństwo
  • wsDualHttpBinding - dwukierunkowa komunikacja 
  • webHttpBinding - przekazuje dane przez HTTP lub HTTPS nie korzystając z formatu protokołu SOAP - przydatny w przypadku gdy klient nie wymaga protokołu SOAP
  • wsFederationHttpBinding - zapewnia bezpieczeństwo pomiędzy różnymi systemami autoryzacji i autentykacji
  • netTcpBinding - przesyłanie danych po TCP
  • netNamedPipeBinding - jeżeli serwis będzie używany na jednym komputerze
  • netMsmqBinding - komunikacja pomiędzy dwoma komputerami
  • netPeerTcpBinding - komunikacja Peer-To-Peer pomiędzy komputerami
  • msmqIntegrationBinding - dla istniejących aplikacji napisanych w C++ używających do komunikacji MSMQ
  • basicHttpContextBinding - wsparcie plików cookie dla protokołu HTTP
  • netTcpContextBinding, netTcpContextBinding, wsHttpContextBinding - umożliwia użycie nagłówka w celu wymiany komunikatów

Contract
Określa operacje udostępniane przez serwis i jest to przeważnie nazwa interfejsu poprzedzona przestrzenią nazw.

Powyższe składowe można zadeklarować zarówno w kodzie jak i konfiguracji XML. XML jest preferowanym sposobem, gdyż w przypadku zmiany np. adresu nie ma konieczności kompilowania całej aplikacji od początku, a jedyna zmiana będzie dotyczyła pliku konfiguracyjnego.

Przykład konfiguracji w XML:

Code:
  <system.serviceModel>
    <services>
      <service name="WcfExposingService.OrderService">
        <endpoint address="http://localhost:8000/OrderService/" binding="basicHttpBinding" 
                  contract="WcfExposingService.IOrderService"/>
      </service>
    </services>
  </system.serviceModel>

Dla jednego serwisu można skonfigurować wiele sposobów komunikacji (endpoint), tak więc do jego serwisu możemy odnosić się poprzez protokół HTTP, TCP czy też inny sposób. W tym przypadku należy pamiętać tylko o tym, aby każdy z tych sposobów posiadał inny adres ekspozycji serwisu.
Aby sobie ułatwić pracę podczas definiowania wielu adresów, można skorzystać z właściwości base address i dodać do listy, bazowe adresy, które następnie zostaną uzupełnione odpowiednią końcówką. Uwaga! w przypadku hostowania na IIS bazowe adresy nie są brane pod uwagę, gdyż są mapowane na ścieżkę wirtualną do katalogu gdzie znajdują się pliki serwisu.
W przypadku adresu bazowego WCF rozpoznaje z jakim bindingiem ma do czynienia a następnie dobiera odpowiedni adres z dostępnej listy.
Przykład takiej konfiguracji:

Code:
    <services>
      <service name="WcfExposingService.OrderService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:17354/OrderService/"/>
            <add baseAddress="net.tcp://localhost:17354/OrderService/"/>
          </baseAddresses>
        </host>
        <endpoint address="" binding="basicHttpBinding"
                  contract="WcfExposingService.IOrderService"/>
        <endpoint address="secure" binding="wsHttpBinding"
                  contract="WcfExposingService.IOrderService"/>
        <endpoint address="" binding="NetTcpBinding"
          contract="WcfExposingService.IOrderService"/>
      </service>
    </services>

Oraz konfiguracja w kodzie C#:

Code:
            Uri httpAddress = new Uri("http://localhost:8000/OrderService/");
            Uri tcpAddress = new Uri("net.tcp://localhost:8000/OrderService/");
            Uri[] baseAddresses = {httpAddress, tcpAddress};

            var serviceHost = new ServiceHost(typeof (OrderService), baseAddresses);
            var basicHttpBinding = new BasicHttpBinding();
            var wsHttpBinding = new WSHttpBinding();
            var netTcpBinding = new NetTcpBinding();

            serviceHost.AddServiceEndpoint(typeof (IOrderService), basicHttpBinding, "");
            serviceHost.AddServiceEndpoint(typeof(IOrderService), wsHttpBinding, "secure");
            serviceHost.AddServiceEndpoint(typeof(IOrderService), netTcpBinding, "");



Publikowanie metadanych
Metadane publikujemy za pomocą bindowania o typie mexHttpBinding. Metadane możemy pobrać dla serwisu za pomocą składni: nazwa_serwisu.svc?wsdl. Definiowanie w XML:

Code:
  <system.serviceModel>
    <services>
      <service name="WcfExposingService.OrderService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:17354/OrderService/"/>
            <add baseAddress="net.tcp://localhost:17354/OrderService/"/>
          </baseAddresses>
        </host>
        <endpoint address="" binding="basicHttpBinding"
                  contract="WcfExposingService.IOrderService"/>
        <endpoint address="secure" binding="wsHttpBinding"
                  contract="WcfExposingService.IOrderService"/>
        <endpoint address="" binding="NetTcpBinding"
          contract="WcfExposingService.IOrderService"/>
        <endpoint address="mex" binding="mexHttpBinding"
                  contract="IMetadataExchange" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MexGet">
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

I to samo w kodzie:

Code:
            ServiceMetadataBehavior serviceMetadataBehavior =
                serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
            if (serviceMetadataBehavior != null)
            {
                serviceMetadataBehavior = new ServiceMetadataBehavior();
                serviceMetadataBehavior.HttpGetEnabled = true;
                serviceHost.Description.Behaviors.Add(serviceMetadataBehavior);
            }
            serviceHost.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName,
                                           MetadataExchangeBindings.CreateMexHttpBinding(), "mex");

WCF - Pierwsze kroki

Jakiś czas temu otrzymałem maila z prośbą o napisanie krótkiego kursu w jaki sposób zacząć pracę z WCF.

Do czego służy WCF? Przede wszystkim do pisania aplikacji opartych o architekturę SOA - czyli zorientowaną na usługi. W internecie można znaleźć dokładne opisy czym jest WCF i co potrafi. Najważniejsze jest to, iż za pomocą jednego interfejsu możemy obsługiwać wiele kanałów (soap, tcp, msmq itp).

Kiedyś już pisałem na ten temat - za czasów .NET 3.5. Obecnie programowanie w WCF jest prostsze niż kiedyś. Właściwie po utworzeniu projektu, możemy bezpośrednio uruchamiać serwis bez potrzeby tworzenia jakiejkolwiek konfiguracji.

Aby stworzyć projekt WCF wybieramy odpowiedni szablon w oknie nowego projektu WCF:



Stworzony projekt zawiera już przykładową implementację serwisu. Możemy go bezpośrednio uruchomić (F5) i zobaczyć jak łatwo testuje się serwis WCF:



Jeżeli zobaczymy do pliku web.config:






zauważyć można że nie ma zdefiniowanych żadnych endpointów. Zdefiniowane są mapowania protokołów. Mapowania te znajdują się także w machine.config i są globalne dla wszystkich serwisów. Dzięki temu możemy użyć zdefiniowanych endpointów, Wszystkie dostępne możemy podglądnąć w narzędziu konfiguracyjnym WCF:



Narzędzie uruchamiamy klikając prawym przyciskiem na plik web.config i wybieramy Edit WCF Configuration.

W kolejnej części podstawowe pojęcia, które są związane z WCF.

WCF - Podstawowe pojęcia

Pierwszym podstawowym pojęciem w WCF jest Endpoint.
Endpoint to sposób udostępniania przez serwis swojej funkcjonalności. Definiuje on trzy ważne aspekty:



Mówi się czasami o zasadzie ABC, gdyż od tych liter pochodzą składowe Endpointa:

  • Address - definiuje adres pod jakim znajduje się serwis
  • Binidng - kanał komunikacji (np. tcpip, http)
  • Contract - typ wysyłanej wiadomości


Kolejnym ważnym terminem jest Service Behaviours. Poprzez ten mechanizm możemy kontrolować w jaki sposób będzie się zachowywał serwis.  Możemy wyszczególnić dwa aspekty, na które mamy wpływ - sposób tworzenia serwisu oraz możliwość wykonywania operacji wielowątkowo.



Dostępne tryby tworzenia serwisu:
  • PerCall - przy każdym żądaniu klienta tworzona jest nowa instancja serwisu - domyślna wartość
  • Single - wszystkie żądania obsługiwane są przez tę samą instancję serwisu - Singleton 
  • PerSession - nowa instancja serwisu tworzona jest dla każdej nowej sesji


Kontrolować wielowątkowość możemy wybierając odpowiednią opcję z pośród dostępnych:
  • Single - brak wielowątkowości 
  • Multiple - wielowątkowość włączona
  • Reentrant - jest to dosyć ciekawy przypadek. Działa on w ten sposób, jeżeli klient 1 wywołuje operację w serwisie, a operacja ta odnosi się do innego serwisu, to w tym czasie klient 2 może wywołać operację bez czekania na to aż zostanie zwolniony serwis przez pierwszego klienta. 


Wiadomości między klientem a serwisem wymieniane są za pomocą wiadomości. Wiadomość może być przesyłana w trzech dostępnych trybach:
  • One way - klient wysyła wiadomość i nie czeka na odpowiedź od serwisu 
  • Duplex - obustronna komunikacja między klientem i serwisem 
  • Request-Replay - po wysłaniu żądania przez klienta, zostaje odesłana odpowiedź z serwera

Dodatkowo możemy się spotkać z terminem Channel (kanał) - czyli kanał komunikacyjny (tcp, http, itp.).

Dynamiczne wybieranie endpointa

Jeżeli nasz serwis udostępnia endpoint mex-owy, klienci mają możliwość wyboru dynamicznego endpointa. Nie jest wymagane w tym przypadku tworzenie u klientów konfiguracji. Wystarczy podać adres endpointa mex-owego a wszystkie pozostałe endpointy zostaną automatycznie pobrane.

Aby tego dokonać po pierwsze serwis musi udostępnić endpoint mex. Jeżeli już mamy taki endpoint, na kliencie korzystamy z klasy MetadataResolver i metody Resolve. Metoda ta przyjmuje dwa parametry:
  • typ serwisu
  • adres endpointa mex
Po wywołaniu tej metody zostanie zwrócona kolekcja endpointów obsługiwanych przez aplikację. Możemy przejść po tej kolekcji i wybrać dowolny endpoint, który jest nam najwygodniej stosować. 
Przykładowy kod realizujący dynamiczne odnajdowanie endpointów:

 

Code:
            var serviceEndpointCollection = MetadataResolver.Resolve(typeof(IService1), new EndpointAddress("http://localhost/MyService/mex"));
            ChannelFactory<IService1> channelFactory = null;
            foreach (var endpoint in serviceEndpointCollection)
            {
                if (endpoint.Binding.GetType() == typeof(BasicHttpBinding))
                {
                    channelFactory = new ChannelFactory<IService1>(endpoint.Binding, endpoint.Address);
                    var channel = channelFactory.CreateChannel();
                    var data = channel.GetData(10);
                    Console.WriteLine(data);
                    //Close channel
                    ((IClientChannel)channel).Close();
                    //Close factory
                    channelFactory.Close();
                }
            }


W przykładzie pokazanym powyżej iterujemy po kolekcji zwróconych endpointów. W przypadku gdy odnajdziemy BasicHttpBinding tworzymy kanał a następnie wykonujemy operację.

 Zaletą tego rozwiązania jest przede wszystkim brak potrzeby trzymania dodatkowej konfiguracji endpointów w aplikacji. Wystarczy tylko adres endpointa mex, a pozostałe informacje zostaną pobrane w czasie działania programu. 

Konsumowanie serwisu

Aby można było korzystać z serwisu można zastosować jedną z kilku dostępnych metod. W zależności od naszych preferencji możemy skorzystać z następujących narzędzi:
  • bezpośrednio z klasy ChannelFactory<T> kiedy mamy dostęp do interfejsu servisu
  • dodanie referencji do serwisu z poziomu Visual Studio
  • narzędzie svcutil.exe
Pierwszy sposób jest bardzo wygodny. Wymaga jednak współdzielenia biblioteki zawierającej interfejs serwisu co nie zawsze jest możliwe do osiągnięcia.
Użycie klasy jest niezwykle proste:


Code:
            var channelFactory = new ChannelFactory<IService1>(new BasicHttpBinding(),
                new EndpointAddress("http://localhost/MyService/"));
            var channel = channelFactory.CreateChannel();
            var data = channel.GetData(10);
            Console.WriteLine(data);
            //Close channel
            ((IClientChannel)channel).Close();
            //Close factory
            channelFactory.Close();

Należy przy tym pamiętać, że kanał należy zamknąć w celu odzyskania zasobów. Drugą ważną informacją jest to, iż gdy wystąpi jakiś błąd, nie możemy użyć metody Close. W takim przypadku należy skorzystać z metody Abort.
W pokazanym przypadku powyżej, dane konfiguracyjne zostały podane w kodzie. Klasa ChannelFactory<T> posiada klika różnych konstruktorów. Jeden z nich przyjmuje nazwę konfiguracji z app/web config. W takim przypadku możemy konfigurację zapisać w pliku i zmienić ją w czasie działania programu bez potrzeby jej ponownej kompilacji. 

Dodanie referencji z poziomu Visual Studio jest bardzo proste. Klikamy prawym klawiszem myszy na projekt i dodajemy Service Reference:






W oknie które się pojawi możemy wpisać adres serwisu lub, jeżeli serwis jest uruchomiony na tej samej maszynie wystarczy kliknąć przycisk Discover. Spowoduje to odnalezienie wszystkich serwisów udostepnionych na tej maszynie:


Po kliknięciu w przycisk OK zostaną wygenerowane klasy typów oraz klasa Proxy zawierająca metody interfejsu. 

Kolejną możliwością jest użycie narzędzia svcutil.exe (ServiceModel Metadata Utility Tool). Jest to proste narzędzie konsolowe. Znaleźć je możemy tam gdzie zainstalowane jest Microsoft SDKs. W moim przypadku jest to ścieżka: C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\NETFX 4.0 Tools.
Narzędzie jest bardzo proste w obsłudze. Przykład:


Narzędzie udostępnia wiele różnych przełączników umożliwiających m.in. wybranie serializatora, podanie danych uwierzytelniania itp.

Po zakończeniu działania programu w wybranym katalogu zostaną wygenerowane dwa pliki: .cs z kodem dla klienta oraz plik konfiguracyjny (dane dotyczące endpointa):



Na temat narzędzia svcutil można bardzo dużo informacji znaleźć w internecie jak i na stronach msdn.

Który sposób jest najlepszy? Jeżeli mamy bezpośredni dostęp do interfejsu - najwygodniejszym narzędziem będzie ChannelFactory<T>. W przypadku gdy nie mamy takiego dostępu możemy skorzystać z Visuala lub svcutil.exe. To drugie narzędzie jest szczególnie przydatne jeżeli chcemy dostosować wygenerowany kod do naszych potrzeb, a w przypadku aktualizacji chcemy mieć nad tym pełną kontrolę.

Obsługa wyjątków w WCF

WCF pozwala na przekazywanie wyjątków do klienta. Oczywiście wyjątki, które znamy z .NET nie możemy wprost przesyłać do klienta - należy pamiętać, że nasz serwis może być obsługiwany przez klientów innych platform.
Wyjątki którymi się posługujemy w WCF są typu FaultException oraz generyczna odmiana FaultException<T>. Aby zaznaczyć, że metoda może zwrócić wyjątek należy oznaczyć ją atrybutem FaultContract(typeof(ExceptionType)):


Code:
        [OperationContract]
        [FaultContract(typeof(string))]
        string GetData(int value);

Metodę następnie możemy zaimplementować w następujący sposób:


Code:
        public string GetData(int value)
        {
            if (value <= 0)
            {
                throw new FaultException<string>("Value must be greater than 0", "Incoret value");
            }
            return string.Format("You entered: {0}", value);
        }

Ogólnie przyjęte jest, że nie należy wysyłać do klienta oryginalnego wyjątku. Zobaczmy na poniższy diagram:



Z diagramu wynika, że oryginalny wyjątek przed wysłaniem do klienta musi zostać przetworzony. Nie przekazujemy oryginalnego wyjątku klientowi, aby nie ujawniać mu wewnętrznej struktury naszego serwisu. Ogólnie przyjmuje się, że po stronie serwisu:
  • przechwytujemy zawartość wyjątku, kto i kiedy go spowodował oraz oryginalne żądanie
  • logujemy zawartość Exception do bazy lub pliku
  • tworzymy identyfikator zgłoszenia - ułatwi to rozmowę pomocy technicznej z danym klientem
Klientowi przekazujemy następujące informacje:
  • wiadomość typu: "W serwisie wystąpił błąd..."
  • kod błędu (np HTTP 5xx)
  • dodajemy także informacje kontaktowe z pomocą techniczną oraz identyfikator problemu
Powyższe działania określane są często mianem Sanitizing Response - tłumacząc na polski usuwamy z odpowiedzi to co nie jest potrzebne klientowi i potencjalnie szkodliwe dla nas.


Aplikacja kliencka następnie może przechwycić wyjątek, tak jak każdy inny. Przykładowy kod klienta:


Code:
            var channelFactory = new ChannelFactory<IService1>(new BasicHttpBinding(),
                new EndpointAddress("http://localhost.:30000/MyService/"));
            var channel = channelFactory.CreateChannel();
            try
            {
                var data = channel.GetData(-5);
                Console.WriteLine(data);
            }
            catch (FaultException<string> ex)
            {
                Console.WriteLine(ex.Reason);
                Console.WriteLine(ex.Detail);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            ((IClientChannel)channel).Close();

WSDL vs MEX

Aby zewnętrzni producenci oprogramowania mogli skorzystać z naszego serwisu muszą w jakiś sposób pobrać informacje o formacie przesyłanych wiadomości.

Do udostępniania opisu serwisu możemy wykorzystać WSDL (Web Services Description Language) lub MEX (Metadata Exchange Endpoint). Jedno jak i drugie rozwiązanie tworzy opis serwisu - ten sam. Różnica polega na tym, iż w przypadku MEX tworzony jest specjalny endpoint, który zapytany o opis serwisu zwraca opis jako jedna wiadomość SOAP.

Podsumujmy
WSDL:
  • dokument WSDL dostępny jest poprzez wysłanie zapytania HTTP GET - wyświetlić możemy go w przeglądarce
  • dokument WSDL może zawierać odnośniki do innych dokumentów (np. schemy) - podział na wiele dokumentów

MEX:
  • tworzony jest specjalny endpoint udostępniający informacje w formacie SOAP
  • jako odpowiedź otrzymujemy jeden dokument zawierający wszystkie dane o serwisie - brak podziału na wiele dokumentów jak w przypadku WSDL
  • pozwala pobrać klientowi informacje o udostępnianych endpointach co pozwala uniknąć przechowywania konfiguracji ich w konfiguracji aplikacji 
  • możliwość pobrania za pomocą tcp, http, https, named pipes

Kiedy użyć MEX a kiedy WSDL?
WSDL generalnie używamy kiedy nasz serwis korzysta z komunikacji za pomocą HTTP. W przypadku użycia tcp, named pipes czy też innego protokołu - udostępniamy opis serwisu za pomocą  MEX.
Większość napisanych już serwisów korzysta z WSDL - standard ten istnieje na rynku już od dłuższego czasu.


Znając różnice zobaczmy w jaki sposób aktywować udostępnianie WSDL oraz MEX.
WSDL:
W przypadku tworzenia projektu na starcie obsługa WSDL jest włączona domyślnie - nie jest wymagana żadna dodatkowa konfiguracja. Wpis który mówi o aktywności WSDL jest następujący:

<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>


Brak potrzeby definiowania konfiguracji zawdzięczamy domyślnym endpointom, które zostały wprowadzone w wersji 4.0 frameworka.


MEX
Podobnie jak powyższy także nie wymaga żadnej dodatkowej konfiguracji - dostępny jest domyślnie. Jeżeli chcielibyśmy hostować aplikację z udostępnianiem MEX w aplikacji konsolowej, moglibyśmy to zrobić w następujący sposób:


Code:
        static void Main(string[] args)
        {
            var serviceHost = new ServiceHost(typeof(Service1), new Uri("http://localhost/MyService"));
            var smb = serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>() ?? new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
            serviceHost.Description.Behaviors.Add(smb);
            serviceHost.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName,
                MetadataExchangeBindings.CreateMexHttpBinding(), "mex");
            serviceHost.AddServiceEndpoint(typeof(IService1), new BasicHttpBinding(), "");
            serviceHost.Open();
            foreach (var endpoint in serviceHost.Description.Endpoints)
            {
                Console.WriteLine(endpoint.Address);
            }
            Console.WriteLine("Press key to exit");
            Console.ReadLine();
            serviceHost.Close();
        }


Kod został zaczerpnięty z dokumentacji MSDN.

Podsumowując: WCF umożliwia udostępnianie metadanych na dwa sposoby: