Spis treści:

Kategoria:C#Windows Communication Foundation


Wyjątki w WCF

Sytuacje wyjątkowe mogą się zdarzyć w wielu miejscach zwykłego kodu, a co za tym idzie, w wielu miejscach usług WCF. Mam na myśli tutaj kod, który sami piszemy, definiując w ten sposób funkcje usługi. Natura wyjątków w kodzie .NET, wywodzących się z klasy Exception, jest nieco inna niż wyjątki WCF. Pamiętajmy, że pomiędzy klientem WCF a usługą jest sieć. Nie jesteśmy wobec tego w ramach jednej platformy .NET, zatem obiekt wyjątku klasy wyprowadzonej z Exception nie może w takiej postaci istnieć. Musi zostać przetłumaczony na coś, co może zostaćprzesłane siecią. Co to jest? Przeanalizujmy po kolei elementy całej układanki.

Podstawy wyjątków

Logicznie rzecz ujmując: skoro WCF działa w oparciu o model SOAP to i błędy też muszą być w ten sposób reprezentowane. Ci, którzy tak pomyśleli mają rację. Błędy reprezentowane są przez zwykłe komunikaty. Dane na temat struktury tych komunikatów (metadane) umieszczone są w definicji usługi razem z danymi dotyczącymi rodzaju udostępnianych funkcji, liczebności i typu parametrów tych funkcji. Dzięki takiej strukturze klient może rozpoznać wyjątek i lepiej go obsłużyć. Dane wyjątku mogą bowiem zawierać oprócz samej informacji o istnieniu błędu dodatkowe informacje, tj. powód błędu, jego rodzaj, dozwolone zakresy parametrów lub wymagane prawa do wywołania metody. Wyjątki serializowane są podobnie jak inne paramatery. W usługach udostępnianych szerokiej rzeszy użytkowników będzie to zatem zwykły XML.

Co zatem sprawia, że należy ich używać? Skoro jest to zwykły XML, to nie możemy sobie najzwyczajniej w świecie przesłać swojego własnego komunikatu z informacją o błędzie? Otóż możemy. Nie ma żadnych przeciwwskazań. Co zatem sprawia, że warto ich używać? Wpływ na to ma kilka czynników:

  • Definicja interfejsu zawiera tylko dane wymieniane przez usługę i klienta. Wyjątki doczepione są niejako obok całej definicji, w postaci atrybutów. Sprawia to, że kod staje się czytelniejszy.
  • Dzięki wyjątkom WCF możemy pisać kod prawie tak samo jak w przypadku zwykłego kodu. Kod serwera może zawierać instrukcję throw, kod klienta usługi blok try - catch.
  • Cała infrastruktura jest zaszyta wewnątrz mechanizmów WCF i nie wymaga od nas prawie żadnego dodatkowego wysiłku.

Teoria teorią, ale spróbujmy napisać jakiś fragment rzeczywistego kodu generującego wyjątek.

Przykład usługi WCF generującej wyjątki

Na pierwszy rzut pójdzie jak zwykle interfejs. Stwórzmy sobie zatem nowy projekt i nazwijmy go WcfExceptionInterface. Kolejnym etapem będzie umieszczenie w tym projekcie definicji interfejsu. Przyjrzyjmy się następującemu przykładowi:

[ServiceContract]
public interface IExceptionServer
{
  [OperationContract]
  double Div(double a, double b);
}

Usługą będzie tym razem genialna w swej prostocie funkcja pozwalająca podzielić dwie liczby. Aby ten interfejs jakoś działał (sam interfejs nam nic nie policzy), potrzebujemy kodu serwera. Stwórzmy drugi projekt, nazwijmy go WcfExceptionServer i wstawmy tam jeden plik z klasą. Przykładowy kod tej klasy mógłby wyglądać następująco:

public class ExceptionServer : IExceptionServer
{
  public double Div(double a, double b)
  {
    if (b == 0.0)
      throw new FaultException("Nieprawidłowe dzielenie przez zero");
    return a / b;
  }
}

Potrzebujemy jeszcze funkcji Main, która będzie przetwarzała poszczególne żądania. W przykładzie pominięte zostały pliki konfiguracyjne, dzięki czemu program jest bardziej spójny. Przykładowy kod metody Main mógłby w takim przypadku wyglądać następująco:

static void Main(string[] args)
{
  Uri adres = new Uri("http://localhost:2222/Exception");
  ServiceHost host = new ServiceHost(typeof(ExceptionServer), adres);
  host.AddServiceEndpoint(typeof(IExceptionServer), new BasicHttpBinding(), adres);
  host.Open();
  Console.WriteLine("Serwer uruchomiony");
  Console.ReadLine();
}

Należy pamiętać o dodaniu odpowiednih referencji do projektów, tj. System.Service.Model oraz System.Runtime.Serialization do projektu z interfejsem oraz System.Service.Model i WcfExceptionInterface do definicji serwera. Tak napisany i skompilowany serwer można już śmiało uruchomić. Warto zwrócić uwagę na kod metody Div. Jeżeli drugi parametr jest równy 0.0, generowany jest wyjątek typu FaultException. Jest to domyślny wyjątek SOAP i jest wspierany przez WCF bez żadnych dodatkowych opracji (o jakie operacje chodzi dowiemy się w dalszej części). Jeden z najprostszych konstruktorów przyjmuje dowolny tekst, który jest komunikatem błędu.

Pozostaje w zasadzie tylko jedno pytanie: jak obsłużyć taki wyjątek po stronie klienta? Popatrzmy na listing zamieszczony poniżej:

static void Main(string[] args)
{
  Uri adres = new Uri("http://localhost:2222/Hello");
  using (var c = new ChannelFactory<IHelloWorld>(
    new BasicHttpBinding(),
    new EndpointAddress(adres)))
  {
    var s = c.CreateChannel();
    try
    {
      Console.WriteLine(s.Div(10.0, 4.0));
      Console.WriteLine(s.Div(15.0, 0.0));
    }
    catch (FaultException ex)
    {
      Console.WriteLine("Wyjątek: " + ex.Message);
    }
    Console.ReadLine();
  }
}

Przykład jest bajecznie prosty, ale taki właśnie miał być. Miał w jak najbardziej przejrzysty sposób pokazać wygenerowanie wyjątku, przekazywanie wyjątku od serwera do klienta i obsługa tego wyjątku po stronie serwera. Muszę jeszcze wyjaśnić, skąd wzięła się klasa FaultException. Można, co wydaje się naturalne, przechwytywać zwykły bazowy wyjątek klasy Exception. Na tym etapie komunikat SOAP jest już całkowicie zamieniony na wyjątek obiektowy. FaultException dziedziczy pośrednio z Exception, więc jawne wskazywanie FaultException w bloku catch nie jest konieczne. Dziedziczenie można przedstawić taką oto ścieżką Exception => SystemException => CommunicationException => FaultException.

Jeżeli wszystkie powyższe elementy infrastruktury wyjątków są zrozumiałe, można przystąpić do bardziej zaawansowanych technik. Co mamy zrobić, gdy zachodzi konieczność przesłania dodatkowych informacji związanych z błędem, na przykład identyfikator błędu?

WCF i wyjątek z dodatkowymi parametrami

Obsługa wyjątków pozwalających przesłać tylko tekst z pewnością nie wyczerpuje tematu informowania o błędach. Aby przesłać przez sieć wyjątek z dodatkową wartością możemy posłużyć się klasą ogólną, tj. FaultException<T>. Jak to działa? Przypuśćmy, że usługa WCF zwraca nam wyjątek z parametrem określającym numer błędu. Aby nie pisać dodatkowego kodu, metoda będzie przyjmować liczbę typu int i zwracać wyjątek zawierający tę samą liczbę. Co trzeba zrobić, aby cieszyć się takimi rozwiązaniami? Przyjrzyjmy się zmianom, jakie należy wprowadzić do napisanego już kodu. Pierszym etapem jest zmiana interfejsu:

[ServiceContract]
public interface IExceptionServer
{
  [OperationContract]
  double Div(double a, double b);

  [OperationContract]
  [FaultContract(typeof(int))]
  void ThrowIntException(int i);
}

Fragment, na który należy zwrócić szczególną uwagę, został pogrubiony. Aby biblioteki WCF wiedziały, co jest tym dodatkowym elementem (ten dodatkowy element wymaga biwiem serializacji do postaci XML komunikatu SOAP), należy wstawić atrybut FaultContract, podając w postaci parametru typ dodatkowego pola. Całą resztę przejmuje na siebie WCF. My musimy zaimplementować samą metodę ThrowIntException. Tego WCF za nas nie zrobi. Kod serwera mógłby wyglądać tak, jak poniżej:

public class ExceptionServer : IExceptionServer
{
  public double Div(double a, double b)
  {
    if (b == 0.0)
      throw new FaultException("Nieprawidłowe dzielenie przez zero");
    return a / b;
  }

  public void ThrowIntException(int i)
  {
    throw new FaultException<int>(i);
  }
}

W kodzie serwera nie ma żadnych istotnych zmian. Trochę mniej wygodny jest sposób wyrzucania wyjątku, ale nie odbiega on on standardowego sposobu tworzenia wystąpień klas typowanych. Przejdźmy zatem do wyłapania tego błędu w kodzie klienta:

static void Main(string[] args)
{
  Uri adres = new Uri("http://localhost:2222/Hello");
  using (var c = new ChannelFactory<IHelloWorld>(
    new BasicHttpBinding(),
    new EndpointAddress(adres)))
  {
    var s = c.CreateChannel();
    try
    {
      Console.WriteLine(s.Div(10.0, 4.0));
      Console.WriteLine(s.Div(15.0, 0.0));
    }
    catch (FaultException ex)
    {
      Console.WriteLine("Wyjątek: " + ex.Message);
    }

    try
    {
      s.ThrowIntException(997);
    }
    catch (FaultException<int> ex)
    {
      Console.WriteLine("Wyjątek: " + ex.Detail);
    }

    Console.ReadLine();
  }
}

Cały trik polega na przechwyceniu odpowiedniego typu wyjątku. Nie powinniśmy stosować tutaj prostego typu Exception. Aby mieć dostęp do dodatkowej informacji, musimy mieć odpowiednią klasę. Co więcej, FaultException<T> jest typem ogólnym. Ogólna jest też właściwość Detail w typie FaultException<T>. Aby uzyskać do niej dostęp typowany, należy w filtrze wyjątków umieścić wersję typowaną. Użycie typu Exception jest możliwe, ale mocno utrudniony jest wtedy dostęp do danych dodatkowych, a o te dane dodatkowe nam przecież chodziło.

Idąc dalej w stronę rozszerzania wyjątku możemy zapytać: jak przesłać razem z wyjątkiem klasę, a w klasie kilka pól? Dałoby nam to w zasadzie nieograniczone możliwości przesyłania informacji razem z wyjątkiem. Na takie pytanie odpowiedź jest prosta. Znów wystarczy zastosować FaultException<T>, pamiętając jednocześnie o kilku rzeczach.

Przesyłanie klas w wyjątkach WCF

Dokładając kolejne fragmenty do poszczególnych klas utworzyliśmy prawie pełny przykład przesyłania wyjątków. Brakuje nam jeszcze tylko przykładu przesyłania pełnej klasy. Aby ją przesłać, należy ją wcześniej napisać. Niech będzie ona odpowiedzialna za przesyłanie informacji o rodzaju błędu w bazie danych. Kod mógłby wyglądać następująco:

[DataContract]
public class DatabaseException
{
  [DataMember]
  public int DbInternalCode { get; private set; }
  public DatabaseException(int dbCode)
  {
    DbInternalCode = dbCode;
  }
}

Klasę warto umieścić w projekcie z interfejsem, bo jest to część wspólna dla klienta i serwera.

Przedstawmy sobie teraz pełny kod interfejsu:

[ServiceContract]
public interface IExceptionServer
{
  [OperationContract]
  double Div(double a, double b);

  [OperationContract]
  [FaultContract(typeof(int))]
  void ThrowIntException(int i);

  [OperationContract]   [FaultContract(typeof(DatabaseException))]   decimal GetProductPrice(string productCode); }

Kod samego serwera nie uległ zmianie i został przedstawiony wcześniej. Pełny kod implementacji będzie wyglądał następująco:

public class ExceptionServer : IExceptionServer
{
  public double Div(double a, double b)
  {
    if (b == 0.0)
      throw new FaultException("Nieprawidłowe dzielenie przez zero");
    return a / b;
  }

  public void ThrowIntException(int i)
  {
    throw new FaultException<int>(i);
  }

  public decimal GetProductPrice(string productCode)
  {
    if (productCode == "F116")
      return 5.0M;
    else
    throw new FaultException<DatabaseException>(
      new DatabaseException(404));
  }
}

Sposób wyrzucania wyjątku zawierającego klasę jest nieco rozbudowany. Można sobie ten proces nieco uprościć tworząc statyczną metodę w klasie DatabaseException, która wewnątrz stworzy właściwy obiekt i opakuje go typowaną klasą FaultException. Wszystko zależy od potrzeb.

Na koniec pełny kod klienta:

static void Main(string[] args)
{
  Uri adres = new Uri("http://localhost:2222/Hello");
  using (var c = new ChannelFactory<IHelloWorld>(
    new BasicHttpBinding(),
    new EndpointAddress(adres)))
  {
    var s = c.CreateChannel();
    try
    {
      Console.WriteLine(s.Div(10.0, 4.0));
      Console.WriteLine(s.Div(15.0, 0.0));
    }
    catch (FaultException ex)
    {
      Console.WriteLine("Wyjątek: " + ex.Message);
    }

    try
    {
      s.ThrowIntException(997);
    }
    catch (FaultException<int> ex)
    {
      Console.WriteLine("Wyjątek: " + ex.Detail);
    }

    try
    {
      Console.WriteLine(s.GetProductPrice("F116"));
      Console.WriteLine(s.GetProductPrice("AD2012"));
    }
    catch (FaultException<DatabaseException> ex)
    {
      Console.WriteLine("Wyjątek: " + ex.Detail.DbInternalCode);
    }

    Console.ReadLine();
  }
}

Przedstawione przypadki powinny wystarczyć w zdecydowanej większości przypadków.

Jeżeli ktoś myśli, że to wszystko, co można zrobić z wyjątkami w WCF, to jest w błędzie. Napisałem wcześniej, że większość rzeczy jest przed nami ukryta i wykonuje się automatycznie. WCF jest jednak architekturą mocno otwartą. Istnieje możliwość wciśnięcia własnych metod do środka modułów odpowiedzialnych za przetwarzanie wyjątków, choćby poprzez tzw. Behaviors. Jest to o tyle interesujące, że pozawala zawrzeć całą logikę obsługi błędów serwera w jednym miejscu. Jest to temat z gatunku bardziej zaawansowanych i zostanie przedstawiony w oddzielnym wpisie.

Kategoria:C#Windows Communication Foundation

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?