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

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

Для WCF-службы я определил обработчик ошибок FaultErrorHandler, реализовав интерфейс IErrorHandler.
Сериализация сообщения об ошибке поручена объекту serializer класса WCFExceptionSerializer, наследнику абстрактного класса XmlObjectSerializer. При этом сообщение об ошибке является не FaultMessage, а обычным Message. Для того чтобы клиент мог отличить такое сообщение от обычного сообщения, в заголовок этого сообщения вставляется элемент «error».
Сериализатор WCFExceptionSerializer создаёт элемент с именем, соответствующим типу возникшего исключения. В качестве дочерних элементов записываются сообщение исключения и специфические для него данные (если таковые имеются). Также в это сообщение можно включить такие данные, как StackTrace, Source и т.д. В итоге сообщение выглядит следующим образом:
Подключаем наш обработчик ошибок к службе с помощью поведения для контракта приложения службы ErrorHandlingBehavior.
Подключим поведение ErrorHandlingBehavior к службе, используя механизм атрибутов:
Единственный метод службы MyService выбрасывает то обычное DivideByZeroException исключение, то созданное исключение SomeCustomException, содержащее некоторые дополнительные данные (число с плавающей запятой). В последнем случае сообщение будет выглядеть следующим образом:
Теперь служба готова к работе.
Определим для клиента инспектора сообщений ErrorInspector, который будет получать сообщения от службы и обрабатывать их в случае, если в заголовке сообщения содержится элемент «error».
Добавление нашего инспектора сообщений в поведение ErrorHandlingBehavior происходит в методе ApplyClientBehavior.
Обработку «fault»-сообщения производит метод ExtractException, в нём происходит считывание соответствующих данных из тела сообщения. На основе считанных данных создаётся объект типа WCFException (наследник Exception), различные дополнительные данные заносятся при необходимости в его коллекцию Data.
Теперь остаётся создать клиента WCF-службы и добавить к поведениям контракта для клиентского приложения уже знакомое нам поведение ErrorHandlingBehavior.
В блоке catch перехватывается исключение WCFException и производится его обработка в зависимости от того, какой тип исключения оно представляет.
Тестируем службу и клиента с подключённым новым поведением ErrorHandlingBehavior.
Корректно обрабатывается DivideByZeroException:

и новое SomeCustomException:

При этом мы имеем доступ к любым дополнительным данным, которые может иметь наше произвольное исключение.
Также параллельно уменьшается количество кода, так как для обработки исключений нам не нужно создавать классы-описания исключений (те, которые определяются как FaultContract), и при этом не увеличивается объём сообщений; мы посылаем только те данные, которые нас интересуют.
Среди недостатков можно отметить то, что для добавления нового пользовательского исключения со своей дополнительной информацией в эту систему, необходимо вручную дописать как будет сериализоваться эта информация в WCFExceptionSerializer и десериализоваться в ErrorInspector. Также имя этого нового пользовательского сообщения надо будет поместить в статическое, только-для-чтения поле класса WCFException.
Возможно, эту процедуру можно автоматизировать, чтобы пользоваться этим методом было удобнее.
Скачать исходный код (стандартный метод обработки ошибок)
Скачать исходный код (предлагаемый метод обработки ошибок)
Всё вроде бы шло хорошо – клиент работал как под .NET, так и под Mono – до тех пор, пока я не занялся обработкой исключений, которые могут возникнуть в методах WCF-службы.
Проблемы начались тогда, когда мне понадобилось обработать моё собственное исключение, содержащее не только сообщение исключения, но и некоторую дополнительную информацию.
Я решил, что организую обработку исключительных ситуаций так, как это описано в статье «Exceptions through WCF» (http://habrahabr.ru/blogs/net/41638/) уважаемого Романа RomanNikitin.
В .NET клиент работал так, как надо, а вот при запуске под Mono возникла следующая ошибка:

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

Сконфигурируем 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:

и новое SomeCustomException:

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