Сообщений 4    Оценка 305        Оценить  
Система Orphus

Обработка ошибок в Windows Communication Foundation (WCF)

Автор: Александр Межов
NeoConcept

Источник: RSDN Magazine #4-2007
Опубликовано: 15.03.2008
Исправлено: 10.12.2016
Версия текста: 1.1
Вступление
Концепция обработки ошибок
Ошибки и исключения WCF
Экземпляры сервиса и Singleton
Сервисные ошибки
Контракт ошибок сервиса
Неизвестные ошибки
Отладка ошибок
Детали ошибки
Способы включения деталей
Ошибки и обратных вызовов
Расширенная обработка ошибок
Преобразование исключений
Обработка ошибок
Установка расширенной обработки ошибок
Обратные вызовы и расширенная обработка ошибок
Резюме
Дополнительная информация

Примеры расширенной обработки ошибок

Вступление

О технологии Windows Communication Foundation (WCF) сказано достаточно много. В сети существует множество примеров, начиная от самых простых и заканчивая довольно сложными. И, казалось бы, все уже прекрасно понимают, что представляет собой эта технология, каковы ее преимущества, а главное то, как применять ее в собственных разработках. Поскольку я довольно давно работаю с WCF, мне тоже так казалось, пока не возникла та задача, которая заставила меня, наконец, изучить тонкости WCF. Собственно, знания, которые я приобрел в ходе решения обозначенной задачи, а также личные соображения по этому поводу, я изложу в данной статье.

С обработкой ошибок сталкивался и сталкивается каждый день любой разработчик. И мы к этому привыкли, привыкли делать проверки на null и прочее, чтобы избежать наших «любимых» исключений или сформировать и сгенерировать собственное, более информативное исключение. Но когда речь идет о распределенных приложениях, тут следует учитывать их специфику.

В распределенной системе в качестве источника ошибок может выступать как клиентская, так и сервисная сторона.

Любая операция сервиса может вызвать исключение, о котором бы хотелось как-то оповестить клиента. На этом этапе и начинает работать специфика, о которой я говорил ранее, - специфика передачи ошибок между клиентом и сервисом. О ней и будет разговор.

Концепция обработки ошибок

Говоря о WCF, следует четко понимать основную концепцию сервисов. Сервис в WCF рассматривается, как нечто самодостаточное и совершенно независимое от своих клиентов. К этому и следует стремиться, проектируя собственные сервисы. Помимо этого, сервис должен быть отказоустойчивым, иначе говоря, он не должен переходить в ошибочное состояние в случае какой-либо некритичной для него ситуации. Последнее очень важно в плане понимания идеи обработки ошибок в WCF.

Если в сервисе и происходит исключение, то клиенту не обязательно знать обо всех тонкостях этого исключения, ему важен факт ошибки и основная причина ее возникновения. Все тонкости важны только на этапе отладки, но в реальной жизни они не должны выходить за пределы сервиса. Для того чтобы как-то следить за жизнью сервиса, обычно пользуются журналом событий или определяют собственную реализацию логирования. Об этом мы тоже поговорим, но чуть позже, а пока рассмотрим основные типы исключений в WCF.

Ошибки и исключения WCF

В WCF работа hosting-процесса организована таким образом, что исключения, возникающие в WCF-сервисе, не нарушают работу этого процесса, а также работу других запущенных сервисов и клиентов, не имеющих отношения к этим исключениям. Как и в любой другой технологии удаленного взаимодействия, ошибка сервиса (fault) передается только тому клиенту, который стал ее инициатором.

Ясно, что для передачи ошибки, между клиентом и сервисом должна существовать некоторая договоренность, благодаря которой клиент однозначно воспринимает сервисную информацию и наоборот. Частью такой договоренности служат ошибки, о которых знает и клиентская и сервисная сторона.

Пытаясь обратиться к сервису, клиент, фактически, может столкнуться с тремя типами ошибок:

С первыми двумя типами ошибок, как правило, все понятно, к тому же они обеспечиваются самой инфраструктурой WCF. Что касается ошибок, возникающих при обращении к сервису, – тут все намного сложнее и интереснее, поскольку существует множество причин возникновения ошибки, при выполнении сервисного запроса. В связи с этим, именно ошибки запросов, и представляют наибольший интерес, собственно им и посвящается данная статья.

Текущая реализация логической модели WCF на .NET-платформе такова, что исключения, вызванные на стороне сервиса, как правило, достигают клиента в виде FaultException.

      public
      class FaultException : CommunicationException
{ ... }

Экземпляры сервиса и Singleton

Когда на сервисе происходит исключение, WCF не разрушает сервисный процесс, однако, может воздействовать на экземпляр удаленного объекта и возможность клиента продолжать использовать коммуникационный канал.

Если запрос вызывает непредвиденное исключение, то прокси клиента генерирует FaultException. Как правило, сервисные исключения переводят канал в состояние ошибки, так что даже если отловить такое исключение на клиенте, последующие запросы вызовут CommunicationObjectFaultedException. Однако такое поведение справедливо не всегда, и все во многом зависит от используемого связывания. Так, например, если определить BasicHttpBinding, то после исключения канал остается работоспособным.

Насколько я знаю, многие разработчики часто делают на клиентской стороне singleton-ссылки на WCF-сервисы, чтобы каждый раз не создавать канал. И, несомненно, все, кто так делал, хоть раз сталкивались с вышеуказанной ошибкой. Поэтому следует быть очень внимательным при использовании singleton’ов и, в случае возникновения ошибки канала, заново пытаться получить доступ к сервису. Следующий пример, отчасти демонстрирует подобную логику, хотя и не претендует на звание лучшего.

Пример 1 – Пример избегания ошибки коммуникации
        private ICalculatorContract _сalculatorService;
public ICalculatorContract CalculatorService
{
  get
  {
    if (_сalculatorService == null)
    {
      // получение ссылки на удаленный объект
    }

    return _сalculatorService;
  }
}

publicvoid ClientMethod()
{
  try
  {
    CalculatorService.Divide(9, 0);
  }
  catch (Exception)
  {
    // Коммуникационный канал разрушен
    _сalculatorService = null;
  }
}

Сервисные ошибки

Ошибки, возникающие на стороне сервиса, передается клиентской стороне по сети. Для их передачи в WCF используется протокол SOAP. Таким образом, ошибка, возникшая на сервисе, перед тем, как будет передана клиенту, должна быть приведена к некоторой промежуточной форме, наиболее удобной для передачи. Поскольку клиент может иметь абсолютно любую реализацию (не обязательно на платформе .NET), «обычное» CLR-исключение не может быть передано, как есть, вместо этого по сети передаются только указанные выше типы ошибок.

На первый взгляд такой подход может показаться совершенно неудобным и непрозрачным, но это только первое впечатление. Так или иначе, мы имеем дело с распределенностью. Помимо таких причин, как затраты на сериализацию и десериализацию исключений, объем передаваемых данных и прочее, основной причиной ограничения на типы передаваемых исключений является все же суть построения самих WCF-сервисов: сервис должен быть максимально независим, отказоустойчив, а о тонкостях возникающих в нем ошибок знать никто не должен, поскольку это просто небезопасно.

К счастью разработчиков, есть generic-класс сервисного исключения FaultException<T>, благодаря которому существует возможность передавать некоторые детали возникшей в сервисе ошибки. Этот класс определен следующим образом:

        public
        class FaultException<TDetail> : FaultException
{
  public FaultException(TDetail detail);
  public FaultException(TDetail detail, string reason);
  // ...
}

TDetail – тип, который описывает детали ошибки. Уточняющий тип TDetail не обязательно должен быть представлен классом исключения, но он обязательно должен быть сериализуем и/или определен контрактом данных.

Пример 2 – Применение класса исключения FaultException&lt;T&gt;
[ServiceContract]
interface ICalculator
{
  [OperationContract]
  double Divide(double number1, double number2);
  // ...
}

class Calculator : ICalculator
{
  publicdouble Divide(double number1, double number2)
  {
    if (number2 == 0)
    {
      DivideByZeroException exception = 
        new DivideByZeroException("Деление на ноль!");
      thrownew FaultException<DivideByZeroException>(
exception, exception.Message);
    }
    return number1 / number2;
  }
  // ...
}

В качестве основания ошибки можно передать, например, сообщение исходного исключения, или экземпляр класса FaultReason, который удобно использовать, когда требуется локализация.

Клиент сервиса, приведенного в примере 2, может перехватывать исключение следующим образом:

ICalculator service;
// ...try
{
  service.Divide(7, 0);
}
catch (FaultException<DivideByZeroException>)
{
  // обработка деления на ноль
}
catch (FaultException)
{
  // обработка прочих сервисных исключений
}
catch (Exception)
{
  // обработка иных исключений
}

Контракт ошибок сервиса

По умолчанию любое исключение, возникающее при выполнении сервисной операции, доходит до клиента в виде FaultException. Для того чтобы сервис мог передавать клиенту иные ошибки, они должны быть частью контракта. Контракт ошибок WCF – это способ для сервиса указать список ошибок, которые он может генерировать.

Контракт ошибок является частью контракта сервиса. Для указания исключения, которое может вызвать сервисная операция, последняя описывается атрибутом FaultContractAttribute. Если возвращаться к примеру с калькулятором, то можно было сделать следующее (см. пример 3).

Пример 3 – Пример определения контракта ошибок
[ServiceContract]
interface ICalculator
{
  [OperationContract]
  [FaultContract(typeof(DivideByZeroException))]
  double Divide(double number1, double number2);
  // ...
}

Определив контракт ошибок, вы декларативно задаете, какие исключения может генерировать сервис. Исходя из этой информации, клиентская сторона может правильно трактовать возникающие на сервисе ошибки и как-то их обрабатывать. С другой стороны, это накладывает свои ограничения. Сервисная операция может генерировать исключения только тех типов, которые определены в контракте (нельзя даже использовать подклассы определенного в контракте типа исключения). Для указанного примера, генерация исключения на сервисе могла бы выглядеть так:

DivideByZeroException exception = 
  new DivideByZeroException("Деление на ноль!");
thrownew FaultException<DivideByZeroException>(exception, exception.Message);

Все контрактные ошибки доходят до клиента в виде FaultException<T>, где T – тип исключения, объявленного в контракте ошибок. Если сервис вызовет ошибку, не определенную контрактом, она дойдет до клиента, как FaultException.

ПРЕДУПРЕЖДЕНИЕ

Обратите внимание, что контракт ошибок нельзя определять для «one-way» методов, поскольку в теории они ничего не должны возвращать. В связи с этим, если определить контракт таким образом:

[ServiceContract]

interface IMyContract

{

[OperationContract(IsOneWay = true)]

[FaultContract(...)]

void MyMethod( );

}

при загрузке сервиса произойдет исключение типа InvalidOperationException.

Справедливо будет, если вы сейчас спросите меня, зачем определять контракт ошибок, если он нужен лишь для описания сервисного контракта и для того, чтобы клиент знал о возможных исключениях. На самом деле, это еще не все. Исключение, определенное контрактом ошибок, правильно сериализуется и передается клиентской стороне, что позволяет избежать CommunicationException и последующего разрушения коммуникационного канала. Это отчасти решает проблему, которая возникает при использовании singleton-ссылок на сервисы (см. выше).

Неизвестные ошибки

Как уже стало ясно все исключения, возникающие на сервисной стороне, доходят до клиента в виде FaultException или FaultException<T>. Если уточняющий тип T не определен в контракте ошибок сервиса, ошибка является «неизвестной» и на клиентской стороне будет сгенерировано исключение FaultException.

Как известные, так и неизвестные ошиби не вызывают разрушения коммуникационного канала.

В принципе, рассказ об ошибках можно было закончить и на этом. Но все было задумано для рассмотрения двух основных вопросов – отладка и обработка WCF-ошибок. Теперь, когда все точки расставлены, можно приступить к рассмотрению этих вопросов.

Отладка ошибок

Мы привыкли к тому, что в WCF обращение к удаленным сервисам ничем не отличается от обращения к простым локальным объектам. Это удобно, прозрачно и просто. В связи с этим, когда дело доходит до обработки сервисных исключений, у многих появляется вполне ожидаемое недоумение: «Почему же нельзя было сделать так, чтобы работа с исключениями была также абсолютно прозрачной для пользователей сервиса? Почему возникающие на сервисной стороне исключения не передаются клиенту в том же виде, в каком они возникли изначально, а вместо этого приходит FaultException?».

Ответ на самом деле достаточно прост. Помимо специфики, которая возникает при передаче исключений по сети, есть еще более веские причины – это безопасность и уменьшение связности между сервисом и клиентом. Если здраво рассуждать, клиенту вовсе не нужно знать все детали ошибок, которые происходят на сервисной стороне. Такая необходимость возникает лишь при отладке работы сервиса.

Детали ошибки

Для передачи деталей исключения специально определен класс ExceptionDetail. Он служит своеобразной оберткой истинного сервисного исключения. Иллюстрацией данного механизма служит следующий пример.

Пример 4 – Пример включения деталей сервисной ошибки
[ServiceContract]
interface ICalculator
{
  [OperationContract]
  double Divide(double number1, double number2);
  // ...
}

class Calculator : ICalculator
{
  publicdouble Divide(double number1, double number2)
  {
    if (number2 == 0)
    {
      DivideByZeroException exception = 
        new DivideByZeroException("Деление на ноль!");
      ExceptionDetail detail = new ExceptionDetail(exception);
      thrownew FaultException<ExceptionDetail>(
detail, exception.Message);
    }
    return number1 / number2;
  }
  // ...
}

Как видите, при таком подходе отсутствует острая необходимость определять контракт ошибок. Информация о возникшем исключении будет передана клиентскому приложению в виде FaultException<ExceptionDetail>. Для приведенного примера обращение к сервису может выглядеть следующим образом:

        try
{
  ICalculator service;
  // ...
  service.Divide(9, 0);
}
catch(FaultException<ExceptionDetail> ex)
{
  MessageBox.Show(
    null, 
    string.Format("{0}\n{1}\n{2}", ex.Detail.Type, ex.Detail.Message,
 ex.Detail.StackTrace),
    Application.ProductName, 
    MessageBoxButtons.OK, 
    MessageBoxIcon.Error,
    MessageBoxDefaultButton.Button1);
}

Детали ошибки также содержат информацию обо всех исключениях, которые произошли на сервисе (свойство InnerException класса ExceptionDetail). Последнее дает возможность восстановить на клиенте всю последовательность событий, которые произошли на сервисе.

Способы включения деталей

Понятно, что вышеуказанный пример включения деталей ошибок совершенно неудобен для отладки, поскольку требует модификации кода практически во всей реализации сервисного контракта, что, несомненно, вносит излишнюю сложность.

Существует более простой способ определить подобное поведение. Атрибут ServiceBehavior позволяет автоматически сделать так, чтобы все исключения, возникающие на сервисе и не определенные контрактом ошибок, передавались клиентской стороне в виде исключения типа FaultException<ExceptionDetail>. Следующий пример является полным аналогом примера 4.

Пример 5 – Включение деталей ошибок атрибутом ServiceBehavior
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
class Calculator : ICalculator
{
  publicdouble Divide(double number1, double number2)
  {
    if (number2 == 0)
    {
      thrownew DivideByZeroException("Деление на ноль!");
    }
    return number1 / number2;
  }
  // ...
}

Если же на сервисной стороне возникает исключение, определенное контрактом ошибок, то оно приходит клиенту, как и должно – в виде контрактной ошибки FaultException<T>.

Однако атрибут можно применить, когда сервис находится на этапе разработки. Но что делать, если он уже давно работает и нет никакой возможности изменить его код? В этом случае есть два пути.

Один из способов заключается в программном изменении процедуры открытия хоста:

ServiceHost host = new ServiceHost(typeof(Calculator));
ServiceBehaviorAttribute debuggingBehavior =
  host.Description.Behaviors.Find<ServiceBehaviorAttribute>();
debuggingBehavior.IncludeExceptionDetailInFaults = true;
host.Open();

Второй способ – определить нужное поведение в конфигурационном файле сервисного приложения:

<system.serviceModel>
  <services>
  <service name = "Calculator" behaviorConfiguration = "Debugging">
    ...
  </service>
  </services>
  <behaviors>
  <serviceBehaviors>
    <behavior name = "Debugging">
    <serviceDebug includeExceptionDetailInFaults = "true"/>
    </behavior>
  </serviceBehaviors>
  </behaviors>
</system.serviceModel>

Последнее, на мой взгляд, является наиболее приемлемым, поскольку не требует перекомпиляции сервисного кода.

ПРЕДУПРЕЖДЕНИЕ

Следует обратить внимание на следующее обстоятельство. При определении автоматического включения деталей ошибки, следует обратить внимание на используемое связывание. Например, в случае с BasicHttpBinding наступление непредвиденного исключения не переводит коммуникационный канал в состояние ошибки, в то время, как в случае с WSHttpBinding ситуация совершенно иная. Если же детали ошибки передаются клиентской стороне «ручным» способом (см. пример 4), то канал останется работоспособным.

Ошибки и обратных вызовов

Для обратных вызовов также можно определить контракт ошибок (см. пример 6).

Пример 6 – Пример определения контракта ошибок для callback
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
   [OperationContract]
   void DoSomething();
}

interface IMyContractCallback
{
   [OperationContract]
   [FaultContract(typeof(InvalidOperationException))]
   void OnCallBack();
}

Поэтому обработка исключений callback’а производится схожим образом, то есть так же, как и на клиенте:

        class MyService : IMyContract
{
  publicvoid DoSomething()
  {
    ...

    IMyContractCallback callback = 
      OperationContext.Current.GetCallbackChannel<IMyContractCallback>();

    try
    {
      callback.OnCallback( );
    }
    catch (FaultException<InvalidOperationException> ex)
    {...}
    catch(FaultException ex)
    {...}
    catch(CommunicationException ex)
    {...}
    catch (Exception ex)
    {...}
   }
}
ПРЕДУПРЕЖДЕНИЕ

Обратите внимание, что в случае возникновения непредвиденной ошибки, callback также может вызвать разрушение коммуникационного канала.

Конечно же, не все так просто. И у обратных вызовов есть своя специфика обработки и отладки исключений.

Для передачи деталей ошибки вы также можете использовать класс ExceptionDetail. Тут в вашем распоряжении опять два способа – либо «вручную» формировать исключение FaultException<ExceptionDetail> в реализации обратного вызова, либо определить автоматический режим включения деталей ошибки.

Чтобы определить автоматический режим, вы должны использовать атрибут CallbackBehavior. Это можно сделать программно:

[CallbackBehavior(IncludeExceptionDetailInFaults = true)]
class MyClient : IMyContractCallback
{
  publicvoid OnCallBack()
  {
    ...
    thrownew InvalidOperationException();
  }
}

или определив соответствующее поведение в конфигурационном файле клиентского приложения:

<client>
  <endpoint ... behaviorConfiguration = "Debug" ... />
</client>
<behaviors>
  <endpointBehaviors>
  <behavior name = "Debug">
    <callbackDebug includeExceptionDetailInFaults = "true"/>
  </behavior>
  </endpointBehaviors>
</behaviors>

Расширенная обработка ошибок

WCF позволяет разработчикам не только настраивать заданную по умолчанию обработку исключений, но и определять собственные обработчики, как на сервисе, так и на клиенте.

Примерная схема распространения исключения от сервиса к клиенту приведена на рисунке 1. На нем Ex0 – первоначальное исключение, возникшее на сервисе; Ex1 – исключение, предназначенное для передачи клиентской стороне.


Рисунок 1.- Схема распространения исключения.

Для создания собственного обработчика ошибок необходимо реализовать интерфейс IErrorHandler.

      public
      interface IErrorHandler
{
  void ProvideFault(
Exception error, MessageVersion version, ref Message fault);
  bool HandleError(Exception error);
}

В последующем этот обработчик связывается с диспетчером канала, после чего появляется возможность перехватывать и обрабатывать все исключения, которые возникают на сервисе.

Преобразование исключений

Метод ProvideFault() вызывается сразу после того, как происходит исключение. Суть метода заключается в преобразовании входного исключения в некоторую альтернативную форму, которая распространится дальше по указанной цепочке (см. рисунок 1).

ПРЕДУПРЕЖДЕНИЕ

В связи с тем, что ProvideFault() выполняется в том же потоке, что и клиентский запрос, клиент будет находиться в состоянии ожидания до тех пор, пока этот метод не закончит свою работу. Поэтому следует остерегаться длительного выполнения ProvideFault().

Если реализация метода ProvideFault() будет «пустой», то входное исключение не будет изменено. Для предоставления альтернативного исключения необходимо создать собственное сообщение об ошибке, которая и будет передана клиенту. Следующий пример поясняет вышесказанное.

Пример 7 – Пример реализации метода преобразования исключений ProvideFault()
        class CalculatorErrorHandler : IErrorHandler
{
  publicbool HandleError(Exception error)
  { ... }
  publicvoid ProvideFault(
Exception error, MessageVersion version, ref Message fault)
  {
    // Анализ входного исключенияif (error is DivideByZeroException)
    {
      // Формирование сообщения с альтернативным 
      // исключением (детали не включаем)
      FaultException<DivideByZeroException> faultException = 
        new FaultException<DivideByZeroException>(null, error.Message);
      MessageFault messageFault = faultException.CreateMessageFault();
      fault = 
        Message.CreateMessage(version, messageFault, faultException.Action);
    }
    ...
  }
}

Как вы успели заметить, реализация метода ProvideFault() сводится к анализу входного исключения и формированию альтернативного сообщения об ошибке. В данном примере было сформировано сообщение с исключением FaultException<DivideByZeroException>, которое не включает детали и содержит только текст действительной ошибки.

Понятно, что реализация данного метода может быть абсолютно любой и зависит от специфики самого сервиса.

В редких случаях возникает необходимость в подавлении любых исключений, которые могут случиться на сервисе. Для этого сообщению альтернативной ошибки следует присвоить null:

        class CustomErrorHandler : IErrorHandler
{
  publicbool HandleError(Exception error)
  { ... }
  publicvoid ProvideFault(
Exception error, MessageVersion version, ref Message fault)
  {
    fault = null; // подавление любых ошибок в контракте
  }
}

Такие действия приводят к тому, что любые исключения, возникающие на сервисе, передаются клиенту в виде неизвестной ошибки FaultException, которая не будет нести никакой информации, кроме факта наличия ошибочной ситуации. Тем не менее, важно заметить, что данная ошибка вызовет разрушение коммуникационного канала.

Обработка ошибок

Метод обработки ошибок HandleError() вызывается после передачи управления клиенту и выполняется в отдельном потоке, нежели клиентский запрос и преобразование исключений ProvideFault(). В связи с этим, обработка ошибок может осуществляться даже после передачи управления клиенту, поэтому время её выполнения не столь критично.

ПРИМЕЧАНИЕ

Следует сразу заметить, что HandleError() не оказывает никакого воздействия на клиентское приложение.

Как правило, в реализации обработчика производится запись ошибки в лог.

Пример 8 – Пример реализации обработчика ошибок HandleError()
        class CustomErrorHandler : IErrorHandler
{
  publicbool HandleError(Exception error)
  {
    try
    {
      MyServiceLogging.Log(error);
    }
    catch
    { }
    finally
    {
      returnfalse; // не останавливать вызов расширенной обработки исключений
    }
  }
  publicvoid ProvideFault(
Exception error, MessageVersion version, ref Message fault)
  { ... }
}

Не исключены варианты, когда подмены исключения не происходит, а осуществляется простая запись в лог.

В целом, HandleError() предоставляет очень удобный способ централизованного ведения лога ошибок сервиса. Если раньше запись в лог приходилось делать в реализации каждого метода сервиса в момент генерации исключения (причем в основном потоке), то с использованием такого подхода все это делается в одном месте. Это, с одной стороны, упрощает сервисный код, а с другой – уменьшает время блокировки клиента из-за того, что обработка ошибки производится в отдельном потоке.

Установка расширенной обработки ошибок

Мало написать обработчик, его необходимо еще связать с реализацией сервиса. Это не очень интересное занятие, но в последующем я постараюсь показать, как можно упростить для себя данную процедуру.

Любой диспетчер канала WCF имеет коллекцию обработчиков ошибок:

        public
        class ChannelDispatcher : ChannelDispatcherBase
{
  public Collection<IErrorHandler> ErrorHandlers
  { get; }
  ...
}

Чтобы установить собственную реализацию интерфейса IErrorHandler, требуется лишь добавить ее в эту коллекцию.

Вся сложность состоит в том, что обработчик ошибок может быть добавлен только в промежуток времени между инициализацией хоста и его открытием. Для этого можно, например, создать собственный класс хоста на основе ServiceHost и переопределить метод OnOpening(), который вызывается как раз в нужный момент времени (см. пример 9).

Пример 9. Класс ServiceHost&lt;T&gt; с возможностью добавления обработчиков ошибок.
        /// <summary>
        /// Класс хоста для сервиса
        /// </summary>
        /// <typeparam name="T">Реализация сервисного контракта</typeparam>
        public
        class ServiceHost<T> : System.ServiceModel.ServiceHost
{
  /// <summary>/// Класс поведения для обработчиков ошибок/// </summary>privateclass ErrorHandlerServiceBehavior : IServiceBehavior
  {
    /// <summary>/// Обработчик ошибок/// </summary>private IErrorHandler _errorHandler;

    /// <summary>/// Конструктор/// </summary>/// <param name="errorHandler">Обработчик ошибок</param>public ErrorHandlerServiceBehavior(IErrorHandler errorHandler)
    {
      if (errorHandler == null)
      {
        thrownew ArgumentNullException();
      }

      _errorHandler = errorHandler;
    }

    #region IServiceBehavior Members

    publicvoid ApplyDispatchBehavior(
ServiceDescription serviceDescription, 
      ServiceHostBase 
      serviceHostBase)
    {
      // Все диспетчеры хоста связываем с ранее указанным обработчиком ошибокforeach (ChannelDispatcher dispatcher 
        in serviceHostBase.ChannelDispatchers)
      {
        dispatcher.ErrorHandlers.Add(_errorHandler);
      }
    }

    publicvoid AddBindingParameters(
ServiceDescription serviceDescription, 
      ServiceHostBase serviceHostBase, 
      Collection<ServiceEndpoint> endpoints, 
      BindingParameterCollection bindingParameters)
    { }

    publicvoid Validate(
ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    { }

    #endregion
  }

  /// <summary>/// Конструктор/// </summary>/// <param name="baseAddresses">Базовый адрес</param>public ServiceHost(Uri[] baseAddresses)
    : base(typeof(T), baseAddresses)
  { }

  /// <summary>/// Список поведений для обработчиков ошибок/// </summary>
  List<IServiceBehavior> _errorHandlerBehaviors = 
    new List<IServiceBehavior>();

  /// <summary>/// Добавить обработчик ошибок/// </summary>/// <param name="errorHandler">Обработчик ошибок</param>publicvoid AddErrorHandler(IErrorHandler errorHandler)
  {
    if (State == CommunicationState.Opened)
      thrownew InvalidOperationException("Host is already opened");

    _errorHandlerBehaviors.Add(new ErrorHandlerServiceBehavior(errorHandler));
  }

  /// <summary>/// Обработчик, вызываемый перед открытием хоста/// </summary>protectedoverridevoid OnOpening()
  {
    // В коллекцию поведений хоста добавляем поведение 
    // указанных обработчиков ошибокforeach (IServiceBehavior behavior in _errorHandlerBehaviors)
      Description.Behaviors.Add(behavior);

    base.OnOpening();
  }
}

Чтобы связать диспетчер канала с нужным обработчиком ошибок, для сервиса следует определить «специфическое» поведение. С этой целью в приведенном выше примере был реализован собственный класс поведения ErrorHandlerServiceBehavior, определяющий такую связь в методе ApplyDispatchBehavior().

Перед открытием хоста пользователь может назначить сервису собственные обработчики ошибок, используя метод AddErrorHandler(). Реализация данного метода такова, что сначала производится проверка состояния хоста, и, если он еще не открыт, то для указанного обработчика определяется поведение ErrorHandlerServiceBehavior, которое в последующем будет связано с сервисом.

Перед самым запуском хоста срабатывает обработчик OnOpening(). В нем производится добавление к уже определенным поведениям тех, которые были предназначены для обработчиков ошибок.

Используя определенный таким образом класс ServiceHost<T>, запуск сервиса можно выполнить следующим образом:

ServiceHost<CalculatorService> host;
...
// Добавление обработчика ошибок
host.AddErrorHandler(new CalculatorErrorHandler());
// Запуск сервиса
host.Open();

Вторым способом задания обработчика ошибок для реализации сервиса является декларативное определение его поведения, с помощью специально разработанного атрибута (см. пример 10).

Пример 10. Атрибут, определяющий обработчик ошибок для реализации сервиса.
[AttributeUsage(AttributeTargets.Class)]
publicclass ErrorHandlerBehaviorAttribute : Attribute, IServiceBehavior
{
  /// <summary>/// Обработчик ошибок/// </summary>private IErrorHandler _errorHandler;

  /// <summary>/// Конструктор/// </summary>/// <param name="typeErrorHandler">Тип обработчика ошибок</param>public ErrorHandlerBehaviorAttribute(Type typeErrorHandler)
  {
    if (typeErrorHandler == null)
      thrownew ArgumentNullException();

    _errorHandler = (IErrorHandler)Activator.CreateInstance(typeErrorHandler);
  }

  #region IServiceBehavior Members

  void IServiceBehavior.ApplyDispatchBehavior(
ServiceDescription serviceDescription, 
    ServiceHostBase serviceHostBase)
  {
    // Все диспетчеры хоста связываем с ранее указанным обработчиком ошибокforeach (ChannelDispatcher dispatcher 
      in serviceHostBase.ChannelDispatchers)
    {
      dispatcher.ErrorHandlers.Add(_errorHandler);
    }
  }

  void IServiceBehavior.AddBindingParameters(
ServiceDescription serviceDescription, 
    ServiceHostBase serviceHostBase, 
    Collection<ServiceEndpoint> endpoints, 
    BindingParameterCollection bindingParameters)
  { }

  void IServiceBehavior.Validate(
ServiceDescription serviceDescription, 
    ServiceHostBase serviceHostBase)
  { }

  #endregion
}

Код, приведенный в примере 10, отчасти заимствован из предыдущего примера, поэтому останавливаться на нем не будем. Скажем лишь, чт, если для класса, реализующего сервисный контракт, будет определен данный атрибут, то установка обработчика, тип которого был указан в качестве параметра атрибута, произойдет автоматически (см. пример 11).

Пример 11. Декларативное определение обработчика ошибок для реализации сервиса.
[ErrorHandlerBehavior(typeof(CalculatorErrorHandler))]
publicclass CalculatorService : ICalculatorContract
{
  ...
}

Следует заметить, что при таком подходе процедура запуска сервиса проходит стандартным образом.

ПРИМЕЧАНИЕ

Как вы понимаете, в качестве обработчика ошибок может выступать и реализация сервиса. Иногда это удобно, поскольку в этом случае сервис сам обрабатывает свои ошибки:

[ErrorHandlerBehavior(typeof(CalculatorService))]

public class CalculatorService : ICalculatorContract, IErrorHandler

{

...

}

Резюмируя сказанное, замечу, что первый способ установки обработчика сложен в реализации, но является более гибким, поскольку тут вы во время исполнения можете решать, какой обработчик следует использовать. Что касается второго способа, с декларативным заданием, то он более прост в реализации, но менее гибок. Так или иначе, вы сами должны определить наиболее приемлемый метод в рамках конкретного проекта.

Обратные вызовы и расширенная обработка ошибок

Ранее было сказано, что собственные обработчики ошибок можно определять и на клиентской стороне. Но это касается только тех случаев, когда мы имеем дело с обратными вызовами.

Схема распространения исключения от клиентского callback-объекта к вызывающему сервису не несет в себе ничего нового (см. рисунок 2).


Рисунок 2 - Схема распространения исключения при обратных вызовах.

Как видно, исключение, возникшее в callback-объекте, может быть перехвачено обработчиком ошибок, закрепленным за диспетчером callback-канала. В результате, истинное исключение ExC0 может быть преобразовано этим обработчиком в некоторую альтернативную форму ExC1, которая впоследствии будет передана вызывающему сервису.

ПРИМЕЧАНИЕ

Обычно, результат выполнения обратных вызовов не предоставляет большого интереса. В связи с этим, удобно использовать собственные обработчики callback-ошибок, которые оповещают сервис только о факте исключения на клиенте.

Переходя к практической стороне вопроса, следует заметить, что для создания собственного обработчика ошибок callback-объекта нужно реализовать уже рассмотренный интерфейс IErrorHandler. На этом этапе нет ничего нового. Специфика заключена лишь в том, что созданный таким образом обработчик должен быть связан с диспетчером callback-канала, причем до момента его открытия.

Ниже приведен пример реализации фабрики дуплексного канала, которая была создана на основе стандартного класса DuplexChannelFactory<T>. Она позволяет задавать обработчики ошибок callback-объектов. Здесь также был определен метод добавления обработчика AddErrorHandler() и переопределен метод OnOpening(), вызываемый перед открытием callback-канала.

Пример 12 – Класс ClientDuplexHost&lt;T&gt; с возможностью добавления обработчиков ошибок
        /// <summary>
        /// Фабрика для дуплексного канала
        /// </summary>
        /// <typeparam name="T">Контракт</typeparam>
        public
        class ClientDuplexHost<T> : DuplexChannelFactory<T>
{
  /// <summary>/// Класс поведения для обработчиков ошибок/// </summary>privateclass ErrorHandlerClientBehavior : IEndpointBehavior
  {
    /// <summary>/// Обработчик ошибок/// </summary>private IErrorHandler _errorHandler;

    /// <summary>/// Конструктор/// </summary>/// <param name="errorHandler">Обработчик ошибок</param>public ErrorHandlerClientBehavior(IErrorHandler errorHandler)
    {
      if (errorHandler == null)
      {
        thrownew ArgumentNullException();
      }

      _errorHandler = errorHandler;
    }

    #region IEndpointBehavior Members

    publicvoid ApplyClientBehavior(
ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
      // Связь между обработчиком ошибок и диспетчером callback-канала
    clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(
      _errorHandler);
    }

    publicvoid AddBindingParameters(
ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    { }

    publicvoid ApplyDispatchBehavior(
ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    { }

    publicvoid Validate(ServiceEndpoint endpoint)
    { }

    #endregion
  }

  /// <summary>/// Конструктор/// </summary>/// <param name="callbackInstance">Callback-контекст</param>/// <param name="binding">Связывание</param>/// <param name="remoteAddress">Адрес конечной точки сервиса</param>public ClientDuplexHost(
InstanceContext callbackInstance, 
    Binding binding, 
    EndpointAddress remoteAddress)
    : base(callbackInstance, binding, remoteAddress)
  { }

  /// <summary>/// Список поведений для обработчиков ошибок/// </summary>private List<IEndpointBehavior> _errorHandlerBehaviors = 
    new List<IEndpointBehavior>();

  /// <summary>/// Добавить обработчик ошибок/// </summary>/// <param name="errorHandler">Обработчик ошибок</param>publicvoid AddErrorHandler(IErrorHandler errorHandler)
  {
    if (State == CommunicationState.Opened)
    {
      thrownew InvalidOperationException("Chanel is already opened");
    }

    _errorHandlerBehaviors.Add(new ErrorHandlerClientBehavior(errorHandler));
  }

  /// <summary>/// Обработчик, вызываемый перед открытием хоста/// </summary>protectedoverridevoid OnOpening()
  {
    // В коллекцию поведений добавляем поведения для указанных обработчиков ошибокforeach (IEndpointBehavior behavior in _errorHandlerBehaviors)
    {
      Endpoint.Behaviors.Add(behavior);
    }

    base.OnOpening();
  }
}

Очевидно, вы уже успели заметить, что реализация ClientDuplexHost<T> очень напоминает пример 9, однако здесь есть существенные отличия, диктуемые спецификой обратных вызовов.

Чтобы определить поведение для обработчика ошибок, необходимо реализовать интерфейс IEndpointBehavior. Это поведение определяет взаимосвязь между обработчиком и диспетчером callback-канала в методе ApplyClientBehavior().

Что касается метода OnOpening(), то тут для оконечной точки сервиса определяется дополнительное поведение, специализированное на обработку ошибок callback-объекта.

Открытие канала на клиенте с использованием класса ClientDuplexHost<T> осуществляется следующим образом:

ClientDuplexHost<ICalculatorContract> channelFactory;
...
// Добавление обработчика ошибок
channelFactory.AddErrorHandler(new ClientErrorHandler());
// Создание канала
ICalculatorContract сalculatorService = channelFactory.CreateChannel();

Как и в случае обычных сервисов, для обратных вызовов существует возможность декларативного задания обработчика ошибок. По аналогии следует реализовать специализированный атрибут (см. пример 13).

Пример 13. Атрибут, определяющий обработчик ошибок для реализации callback-контракта.
[AttributeUsage(AttributeTargets.Class)]
publicclass CallbackErrorHandlerBehaviorAttribute 
: Attribute, IEndpointBehavior
{
  /// <summary>/// Обработчик ошибок/// </summary>private IErrorHandler _errorHandler;

  /// <summary>/// Конструктор/// </summary>/// <param name="typeErrorHandler">Тип обработчика ошибок</param>public CallbackErrorHandlerBehaviorAttribute(Type typeErrorHandler)
  {
    if (typeErrorHandler == null)
    {
      thrownew ArgumentNullException();
    }

    _errorHandler = (IErrorHandler)Activator.CreateInstance(typeErrorHandler);
  }

  #region IEndpointBehavior Members

  void IEndpointBehavior.ApplyClientBehavior(
ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  {
    // Связь между обработчиком ошибок и диспетчером callback-канала
    clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(
      _errorHandler);
  }

  void IEndpointBehavior.AddBindingParameters(
ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  { }

  void IEndpointBehavior.ApplyDispatchBehavior(
ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  { }

  void IEndpointBehavior.Validate(ServiceEndpoint endpoint)
  { }

  #endregion
}

Таким образом, для установки обработчика достаточно определить данный атрибут в реализации callback-контракта (см. пример 14).

Пример 14 – Декларативное определение обработчика ошибок для реализации callback-контракта
[CallbackErrorHandlerBehavior(typeof(ClientErrorHandler))]
publicclass Callback : ICallbackContract
{
  ...
}

Тут вы вновь оказываетесь перед выбором: гибкость или простота реализации. В конечном счете, выбор будет за вами, но в данном случае я позволю себе сделать небольшое замечание. Для клиентских обратных вызовов императивный способ задания обработчиков ошибок, как правило, является неоправданным, поэтому чаще всего выбор падает на более легкий способ – декларативный.

Резюме

WCF предоставляет мощные механизмы расширения, позволяющие выполнить существенную настройку среды исполнения. В данной статье была рассмотрена лишь одна возможность использования данного механизма. Обработка ошибок сервиса – это непростой процесс, но те средства, которыми располагает WCF, позволяют внести в него некоторый порядок за счет централизованного управления обработкой ошибок.

Дополнительная информация

  1. Juval Lowy. Programming WCF Services. Chapter 6
  2. Расширение WCF при помощи настраиваемых поведений
  3. Эффективные методики управления экземплярами в WCF-приложениях


Эта статья опубликована в журнале RSDN Magazine #4-2007. Информацию о журнале можно найти здесь
    Сообщений 4    Оценка 305        Оценить