Pull to refresh

Проблемы обработки исключений в WCF под Mono

Reading time 13 min
Views 3.6K
По служебной необходимости мне пришлось столкнуться с задачей создания клиента WCF-службы под Mono 2.6.7.
Всё вроде бы шло хорошо – клиент работал как под .NET, так и под Mono – до тех пор, пока я не занялся обработкой исключений, которые могут возникнуть в методах WCF-службы.
Проблемы начались тогда, когда мне понадобилось обработать моё собственное исключение, содержащее не только сообщение исключения, но и некоторую дополнительную информацию.
Я решил, что организую обработку исключительных ситуаций так, как это описано в статье «Exceptions through WCF» (http://habrahabr.ru/blogs/net/41638/) уважаемого Романа RomanNikitin.
В .NET клиент работал так, как надо, а вот при запуске под Mono возникла следующая ошибка:
image


Изучив различные книги по WCF, статьи в сети Интернет, проанализировав диагностику передаваемых SOAP-сообщений, я пришёл к выводу, что ошибка связана с тем, что Mono-клиент некорректно понимает сообщение, которое является FaultMessage.
Решить эту проблему можно, если принять обязанности по сериализации и десериализации «fault»-сообщений на себя.

Схема решения выглядит следующим образом:
image

Сконфигурируем WCF-службу

Для WCF-службы я определил обработчик ошибок FaultErrorHandler, реализовав интерфейс IErrorHandler.
/// <summary>
  /// Обработчик ошибок в WCF-службе
  /// </summary>
  public class FaultErrorHandler : IErrorHandler
  {
    WCFExceptionSerializer serializer = new WCFExceptionSerializer();

    /// <summary>
    /// Включает обработку, связанную с ошибками, и возвращает значение, которое показывает, прерывает ли диспетчер в определённых случаях сеанс и контекст экземпляра.
    /// </summary>
    /// <param name="error">Исключение, возникшее в каком-либо из методов WCF-службы</param>
    /// <returns>Да/Нет</returns>
    public bool HandleError(Exception error)
    {
      return true;
    }

    /// <summary>
    /// Метод конвертирует возникшее исключение в сообщение клиенту
    /// </summary>
    /// <param name="error">Исключение, возникшее в каком-либо из методов WCF-службы</param>
    /// <param name="version">Версия SOAP-сообщения</param>
    /// <param name="fault">Сообщение, передающееся клиенту</param>
    public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
    {
      fault = Message.CreateMessage(version, String.Empty, error, serializer);
      fault.Headers.Add(MessageHeader.CreateHeader("error", String.Empty, null));
    }
  }


* This source code was highlighted with Source Code Highlighter.


Сериализация сообщения об ошибке поручена объекту serializer класса WCFExceptionSerializer, наследнику абстрактного класса XmlObjectSerializer. При этом сообщение об ошибке является не FaultMessage, а обычным Message. Для того чтобы клиент мог отличить такое сообщение от обычного сообщения, в заголовок этого сообщения вставляется элемент «error».

  /// <summary>
  /// Сериализатор сообщения-исключения
  /// </summary>
  public class WCFExceptionSerializer : XmlObjectSerializer
  {       
    public override bool IsStartObject(System.Xml.XmlDictionaryReader reader)
    {
      return false;
    }

    public override object ReadObject(System.Xml.XmlDictionaryReader reader, bool verifyObjectName)
    {
      return null;
    }

    public override void WriteEndObject(System.Xml.XmlDictionaryWriter writer)
    {
    }

    public override void WriteStartObject(System.Xml.XmlDictionaryWriter writer, object graph)
    {
    }

    /// <summary>
    /// Сериализация исключения
    /// </summary>
    /// <param name="writer">Средство записи сообщения-исключения</param>
    /// <param name="graph">Сериализуемое исключение</param>
    public override void WriteObjectContent(System.Xml.XmlDictionaryWriter writer, object graph)
    {
      Exception e = graph as Exception;

      writer.WriteStartElement(e.GetType().Name);
      writer.WriteElementString("Message", e.Message);      
      if (e is SomeCustomException) writer.WriteElementString("AddData", (e as SomeCustomException).AdditionalData.ToString());
      writer.WriteEndElement();
    }    
  }

* This source code was highlighted with Source Code Highlighter.


Сериализатор WCFExceptionSerializer создаёт элемент с именем, соответствующим типу возникшего исключения. В качестве дочерних элементов записываются сообщение исключения и специфические для него данные (если таковые имеются). Также в это сообщение можно включить такие данные, как StackTrace, Source и т.д. В итоге сообщение выглядит следующим образом:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<error i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns=""></error>
</s:Header>
<s:Body>
<DivideByZeroException xmlns="">
<Message>Попытка деления на нуль.</Message>
</DivideByZeroException>
</s:Body>
</s:Envelope>


* This source code was highlighted with Source Code Highlighter.


Подключаем наш обработчик ошибок к службе с помощью поведения для контракта приложения службы ErrorHandlingBehavior.

  /// <summary>
  /// Поведение службы, регистрирующее наш обработчик ошибок
  /// </summary>
  public class ErrorHandlingBehavior : Attribute, IContractBehavior
  {
    // Регистрация обработчика
    private void ApplyDispatchBehavior(ChannelDispatcher dispatcher)
    {    
      foreach (IErrorHandler errorHandler in dispatcher.ErrorHandlers)
      {
        if (errorHandler is FaultErrorHandler)
        {
          return;
        }
      }
      dispatcher.ErrorHandlers.Add(new FaultErrorHandler());
    }

    //
    // Реализация нашего поведения как ContractBehavior
    //
    void IContractBehavior.AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {      
    }

    void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
clientRuntime.MessageInspectors.Add(new ErrorInspector());
}
    void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
      this.ApplyDispatchBehavior(dispatchRuntime.ChannelDispatcher);
    }

    void IContractBehavior.Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
    {
      return;
    }   
  }

* This source code was highlighted with Source Code Highlighter.


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

[ServiceContract, ErrorHandlingBehavior]
  public interface IMyService
  {
    [OperationContract]
    void MethodWithException();
  }

  public class MyService : IMyService
  {
    public void MethodWithException()
    {
      Random r = new Random(DateTime.Now.Millisecond);
      r.Next(100);
      int a = 30 / (r.Next(100) % 2);
      throw new SomeCustomException("Произошло исключение", 12.987);
    }
  } 


* This source code was highlighted with Source Code Highlighter.


Единственный метод службы MyService выбрасывает то обычное DivideByZeroException исключение, то созданное исключение SomeCustomException, содержащее некоторые дополнительные данные (число с плавающей запятой). В последнем случае сообщение будет выглядеть следующим образом:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<error i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns=""></error>
</s:Header>
<s:Body>
<SomeCustomException xmlns="">
<Message>Произошло исключение</Message>
<AddData>12,987</AddData>
</SomeCustomException>
</s:Body>
</s:Envelope>


* This source code was highlighted with Source Code Highlighter.


Теперь служба готова к работе.

Перейдём к клиенту.

Определим для клиента инспектора сообщений ErrorInspector, который будет получать сообщения от службы и обрабатывать их в случае, если в заголовке сообщения содержится элемент «error».
  /// <summary>
  /// Инспектор сообщений
  /// </summary>
  public class ErrorInspector : IClientMessageInspector
  {
    /// <summary>
    /// Метод возникает после получения какого-либо
    /// сообщения (но не Fault-сообщения) клиентом и позволяет
    /// самостоятельно обработать это сообщение
    /// </summary>
    /// <param name="reply">Сообщение</param>
    /// <param name="correlationState">Объект, возвращаемый пользователем при
    /// вызове метода BeforeSendRequest для данного сообщения (в данном случае никакой роли не играет)</param>
    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
      int errorHeaderIndex = reply.Headers.FindHeader("error", String.Empty);
      if (errorHeaderIndex > -1)
      {
        throw ExtractException(reply.GetReaderAtBodyContents());
      }
    }

    /// <summary>
    /// Метод возникает до посылки какого-либо
    /// сообщения службе и позволяет
    /// самостоятельно обработать это сообщение
    /// </summary>
    public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel)
    {
      return null;
    }

    /// <summary>
    /// Метод извлекает данные о возникшем
    /// в методе службы исключении и помещает их
    /// в объект WCFException.
    /// Дополнительные параметры исключения помещаются
    /// в словарь Data объекта WCFException.
    /// Ключом в словаре для какого-либо параметра служит его имя.
    /// </summary>
    /// <param name="xdr">Средство чтения сообщения</param>
    /// <returns>Объект типа WCFException (Ошибка в методе WCF-службы)</returns>
    private WCFException ExtractException(XmlDictionaryReader xdr)
    {
      WCFException wcfError = new WCFException();
      wcfError.ExceptonType = xdr.Name;

      xdr.ReadToFollowing("Message");
      xdr.Read();
      wcfError.Message = xdr.Value;

      if (wcfError.ExceptonType == WCFException.SomeCustomException)
      {
        xdr.ReadToFollowing("AddData");
        xdr.Read();
        wcfError.Data["AddData"] = xdr.Value;
      }

      return wcfError;
    }
  }

* This source code was highlighted with Source Code Highlighter.


Добавление нашего инспектора сообщений в поведение ErrorHandlingBehavior происходит в методе ApplyClientBehavior.
void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
      clientRuntime.MessageInspectors.Add(new ErrorInspector());
    }


* This source code was highlighted with Source Code Highlighter.


Обработку «fault»-сообщения производит метод ExtractException, в нём происходит считывание соответствующих данных из тела сообщения. На основе считанных данных создаётся объект типа WCFException (наследник Exception), различные дополнительные данные заносятся при необходимости в его коллекцию Data.
  /// <summary>
  /// Исключение в методе WCF-службы
  /// </summary>
  public class WCFException : Exception
  {
    // Типы сообщений с дополнительными параметрами
    // дополнительные параметры помещаются в словарь Data
    // данного объекта
    public static readonly string SomeCustomException = "SomeCustomException";

    /// <summary>
    /// Конструктор без параметров
    /// </summary>
    public WCFException()
      : base()
    {
    }

    /// <summary>
    /// Тип исключения
    /// </summary>
    public string ExceptonType
    {
      get;
      set;
    }

    /// <summary>
    /// Сообщение исключения
    /// </summary>
    public new string Message
    {
      get;
      set;
    }
  }

* This source code was highlighted with Source Code Highlighter.


Теперь остаётся создать клиента WCF-службы и добавить к поведениям контракта для клиентского приложения уже знакомое нам поведение ErrorHandlingBehavior.
      MyServiceClient client = new MyServiceClient();
      client.Endpoint.Contract.Behaviors.Add(new ErrorHandlingBehavior());

      Console.WriteLine("Нажмите Enter, когда служба будет готова");
      Console.ReadKey();
      
      try
      {
        client.MethodWithException();
      }
      catch (WCFException wcfEx)
      {
        Console.WriteLine("Ошибка:");
        Console.WriteLine("Сообщение: " + wcfEx.Message);
        if (wcfEx.ExceptonType == WCFException.SomeCustomException)
          Console.WriteLine("Дополнительные сведения: " + wcfEx.Data["AddData"].ToString());
      }
      catch (Exception e)
      {
        Console.WriteLine(e.Message);
      }
      Console.ReadKey();


* This source code was highlighted with Source Code Highlighter.


В блоке catch перехватывается исключение WCFException и производится его обработка в зависимости от того, какой тип исключения оно представляет.

Тестируем службу и клиента с подключённым новым поведением ErrorHandlingBehavior.
Корректно обрабатывается DivideByZeroException:
image

и новое SomeCustomException:
image

При этом мы имеем доступ к любым дополнительным данным, которые может иметь наше произвольное исключение.
Также параллельно уменьшается количество кода, так как для обработки исключений нам не нужно создавать классы-описания исключений (те, которые определяются как FaultContract), и при этом не увеличивается объём сообщений; мы посылаем только те данные, которые нас интересуют.
Среди недостатков можно отметить то, что для добавления нового пользовательского исключения со своей дополнительной информацией в эту систему, необходимо вручную дописать как будет сериализоваться эта информация в WCFExceptionSerializer и десериализоваться в ErrorInspector. Также имя этого нового пользовательского сообщения надо будет поместить в статическое, только-для-чтения поле класса WCFException.
Возможно, эту процедуру можно автоматизировать, чтобы пользоваться этим методом было удобнее.

Скачать исходный код (стандартный метод обработки ошибок)
Скачать исходный код (предлагаемый метод обработки ошибок)
Tags:
Hubs:
+7
Comments 9
Comments Comments 9

Articles