WCF позволяет реализовать обработку ошибок при помощи контрактов ошибок. Подробней об этой методике можно почитать на RSDN http://www.rsdn.ru/article/dotnet/FaultsWCF.xml или в книге “Основы WCF ” Стива Резника, глава 10 - Обработка исключений. Но там рассматривается только обработка исключений при SOAP коммуникациях, для REST нужна дополнительная реализация.
Ниже я покажу, как можно реализовать обработку ошибок для REST сервиса, который реализует две точки доступа POX и JSON.
Давайте создадим WCF службу ReliableService с единственным сервисом Films, который реализует каталог домашних фильмов. Сервис будет содержать 5 методов:
· Counts – количество фильмов в каталоге
· GetFilm – вернуть описания фильма по ID
· GetFilmNames – вернуть последние добавленные фильмы
· GetFilmsByGenre – вернуть фильмы по жанру
· AddFilm – добавить фильм в каталог
Создаем новый проект WCF Service Application, удаляем существующий сервис из проекта и из web.config. И добавим новый сервис Films. Добавим в интерфейс этого сервиса 5 методов, которые можно вызывать через REST.
[ServiceContract]
public interface IFilms
{
[OperationContract]
[WebGet]
int Count();
[OperationContract]
[WebGet]
FilmEntity GetFilm(string filmID);
[OperationContract]
[WebGet]
List<string> GetFilmNames();
[OperationContract]
[WebGet]
List<FilmEntity> GetFilmsByGenre(FilmGenre genre);
[OperationContract]
[WebInvoke]
void AddFilm(FilmEntity film);
}
[DataContract]
public enum FilmGenre
{
[EnumMember]Action,
[EnumMember]Comedy,
[EnumMember]Drama,
[EnumMember]War
}
[DataContract]
public class FilmEntity
{
[DataMember]public int ID { get; set; }
[DataMember]public string Guid { get; set; }
[DataMember]public string Title { get; set; }
[DataMember]public int Year { get; set; }
[DataMember]public FilmGenre Genre { get; set; }
[DataMember]public string Description { get; set; }
}
Реализуем этот сервис:
public class Films : IFilms
{
private readonly SortedDictionary<string, FilmEntity> _films;
public Films()
{
_films = GetFilms();
}
public int Count()
{
return _films.Count;
}
public FilmEntity GetFilm(string filmGuid)
{
return _films[filmGuid];
}
public List<string> GetFilmNames()
{
List<string> filmNames = new List<string>(_films.Count);
foreach (FilmEntity film in _films.Values)
{
filmNames.Add(film.Title);
}
return filmNames;
}
public List<FilmEntity> GetFilmsByGenre(FilmGenre genre)
{
List<FilmEntity> filmsByGenre = new List<FilmEntity>(_films.Count);
foreach (FilmEntity film in _films.Values)
{
if (film.Genre == genre)
{
filmsByGenre.Add(film);
}
}
return filmsByGenre;
}
public void AddFilm(FilmEntity film)
{
if (string.IsNullOrEmpty(film.Guid))
{
film.Guid = Guid.NewGuid().ToString();
}
_films.Add(film.Guid, film);
}
private SortedDictionary<string, FilmEntity> GetFilms()
{
SortedDictionary<string, FilmEntity> films = new SortedDictionary<string, FilmEntity>();
//fill test data here
return films;
}
}
Приведем секцию system.serviceModel в web.config к следующему виду:
<system.serviceModel>
<services>
<service behaviorConfiguration="ReliableService.FilmsBehavior" name="ReliableService.Films">
<endpoint address="" binding="webHttpBinding" contract="ReliableService.IFilms" behaviorConfiguration="poxBehavior" />
<endpoint address="json" binding="webHttpBinding" contract="ReliableService.IFilms" behaviorConfiguration="jsonBehavior" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
service>
services>
<behaviors>
<serviceBehaviors>
<behavior name="ReliableService.FilmsBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
behavior>
serviceBehaviors>
<endpointBehaviors>
<behavior name="poxBehavior">
<webHttp/>
behavior>
<behavior name="jsonBehavior">
<enableWebScript/>
behavior>
endpointBehaviors>
behaviors>
system.serviceModel>
Осталось захостить нашу слудбу на ISS, скомпилироваться и можно смотреть, что у нас получилось.
Правый клик по сервису и выбираем View in Browser или просто открываем http://localhost/ReliableService/Films.svc. Вы должны увидеть:
Films Service.
You have created a service.
Хорошо, попробуем вызвать любой метод, например
Итак, мы имеем REST сервис, который может возвращать ответ в виде XML или JSON. Теперь давайте посмотрим, что будет если во время работы серсива произойдет ошибка. К примеру если метод GetFilm вызвать без параметра
Request Error
The server encountered an error processing the request. The exception message is 'Value cannot be null. Parameter name: key'. See server logs for more details. The exception stack trace is:
Пользователю лучше не видеть наших внутренних ошибок, давайте для начала отключим показ деталей ошибок в конфигурации, для этого нужно установить
<serviceDebug includeExceptionDetailInFaults="false" />
Теперь мы увидим следующее сообщение:
The server encountered an error processing the request. See server logs for more details.
Лучше, но не информативно. Разработчик, который будет использовать наш сервис, не сможет понять, в чем ошибка и будет просить нас проверить наши логии. Давайте попробуем реализовать контракты ошибок, как это описано в ссылках, которые я приводил выше. Неплохо бы разделить ошибки, в которых виноват клиент и в которых виноват сервер. Для этого будем возвращать типизированные контракты ошибок FaultContract.
Для начала нужно в интерфейсе каждому методу добавить пару атрибутов:
[OperationContract]
[WebGet]
[FaultContract(typeof(ClientError))]
[FaultContract(typeof(ServerError))]
FilmEntity GetFilm(string filmID);
Во вторых реализовать два класса ClientError и ServerError, они будут содержать подробное описание ошибки, к примеру, дата ошибки и сообщения об ошибке для разработчика и конечного пользователя. Т.е. разработчик клиента для нашего сервиса может не придумывать сообщения для пользователя, а использовать готовое сообщение.
[DataContract]
public abstract class CatalogError
{
[DataMember(Order = 1, IsRequired = true)]
public DateTime Date { get; set; }
[DataMember(Order = 2, IsRequired = true)]
public string SystemMessage { get; set; }
[DataMember(Order = 3, IsRequired = true)]
public string CustomerMessage { get; set; }
}
[DataContract]
public class ClientError : CatalogError
{
public ClientError(string systemMessage, string customerMessage)
{
Date = DateTime.Now;
SystemMessage = systemMessage;
CustomerMessage = customerMessage;
}
}
[DataContract(Namespace = GlobalConstants.WebServiceNamespace)]
public class ServerError : CatalogError
{
public ServerError(string systemMessage, string customerMessage)
{
Date = DateTime.Now;
SystemMessage = systemMessage;
CustomerMessage = customerMessage;
}
}
Осталось научить наш сервис возвращать ошибки в таком виде. Для этого нужно создать класс реализующий IErrorHandler, атрибут ErrorHandlerBehavior и добавить атрибут в реализацию сервиса.
public class CatalogErrorHandler : IErrorHandler
{
private const string InternalErrorCustomerMessage = "We are having technical issuse at the moment, please try again.";
public void ProvideFault(Exception ex, MessageVersion version, ref Message fault)
{
FaultException faultException = CreateFaultException(ex);
MessageFault messageFault = faultException.CreateMessageFault();
fault = Message.CreateMessage(version, messageFault, faultException.Action);
}
public bool HandleError(Exception error)
{
//TODO Add saving error here
return true;
}
private static FaultException CreateFaultException(Exception ex)
{
FaultException faultException = new FaultException<ServerError>(
new ServerError(ex.Message, InternalErrorCustomerMessage), InternalErrorCustomerMessage);
return faultException;
}
}
public class ErrorHandlerBehaviorAttribute : Attribute, IServiceBehavior
{
private IErrorHandler _errorHandler;
public ErrorHandlerBehaviorAttribute(Type typeErrorHandler)
{
if (typeErrorHandler == null)
{
throw new ArgumentNullException();
}
_errorHandler = (IErrorHandler)Activator.CreateInstance(typeErrorHandler);
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
{
dispatcher.ErrorHandlers.Add(_errorHandler);
}
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}
}
Не забываем добавить атрибут к реализации сервиса:
[ErrorHandlerBehavior(typeof(CatalogErrorHandler))]
public class Films : IFilms
Если бы мы работали по SOAP, этого было бы достаточно, но для REST это не работает.
Для того что бы и для REST подхода заработала архитектура контрактов ошибок WCF, нам нужно сделать обработчики ошибок, которые сериализуют ошибку в нужный нам формат. Для того что бы служба знала как возвращать ошибки для различных точек доступа, также создадим расширения поведения точек доступа.
Для начал сделаем два дополнительных обработчика ошибок PoxErrorHandler и JsonErrorHandler:
public class PoxErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
FaultException faultException = CatalogErrorHandler.CreateFaultException(error);
// extract the our FaultContract object from the exception object.
var detail = faultException.GetType().GetProperty("Detail").GetGetMethod().Invoke(faultException, null);
// create a fault message containing our FaultContract object
fault = Message.CreateMessage(version, faultException.Action, detail, new DataContractSerializer(detail.GetType()));
// tell WCF to use XML encoding
var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Xml);
fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);
}
}
public class JsonErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
FaultException faultException = CatalogErrorHandler.CreateFaultException(error);
// extract the our FaultContract object from the exception object.
var detail = faultException.GetType().GetProperty("Detail").GetGetMethod().Invoke(faultException, null);
// create a fault message containing our FaultContract object
fault = Message.CreateMessage(version, faultException.Action, detail, new DataContractJsonSerializer(detail.GetType()));
// tell WCF to use JSON encoding rather than default XML
var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);
}
}
Теперь создадим два класа расширителя поведения точек доступа
public class FaultingJsonBehaviorElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(FaultingJsonBehavior); }
}
protected override object CreateBehavior()
{
return new FaultingJsonBehavior();
}
}
public class FaultingJsonBehavior : WebHttpBehavior
{
protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear();
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new JsonErrorHandler());
}
public override WebMessageFormat DefaultOutgoingResponseFormat
{
get
{
return WebMessageFormat.Json;
}
set
{
base.DefaultOutgoingResponseFormat = value;
}
}
}
public class FaultingPoxBehaviorElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(FaultingPoxBehavior); }
}
protected override object CreateBehavior()
{
return new FaultingPoxBehavior();
}
}
public class FaultingPoxBehavior : WebHttpBehavior
{
protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear();
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new PoxErrorHandler());
}
}
Осталось прописать эти раширители в файле конфигурации, перед секцией behaviors добавим секцию extensions
<extensions>
<behaviorExtensions>
<add name="faultingPox" type="ReliableService.FaultingPoxBehaviorElement, ReliableService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<add name="faultingJson" type="ReliableService.FaultingJsonBehaviorElement, ReliableService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
behaviorExtensions>
extensions>
Не забудем использовать эти расширители в точках доступа
<endpointBehaviors>
<behavior name="poxBehavior">
<webHttp/>
<faultingPox/>
behavior>
<behavior name="jsonBehavior">
<enableWebScript/>
<faultingJson/>
behavior>
endpointBehaviors>
Дело сделано, теперь в случае ошибки наши REST точки доступа будут возвращать контракты ошибок в нужном формате. Например для POX вызовем метод GetFilm без параметров
получим следующий результат
<ServerError xmlns="ReliableService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Date>2009-10-27T13:26:07.5407249+02:00Date>
<SystemMessage>Value cannot be null. Parameter name: keySystemMessage>
<CustomerMessage>We are having technical issues at the moment, please try again.CustomerMessage>
ServerError>
Исходный код проекта можно скачать по ссылке:
http://sites.google.com/site/mrdragonden/primery/ReliableService.rar?attredirects=0&d=1
Спасибо! Реально помогло особенно последняя часть с наследованием от WebHttpBehavior и переопледелением DefaultOutgoingResponseFormat! Этой части мне как раз и не хватало, чтобы заставить мой wcf rest сервис возвращать ошибки в json.
ОтветитьУдалитьЕдинственное хочу заметить, что в примере присутствует дублирование:
1) В конфиге нужно оставить только
faultingPox и убрать webHttp, так FaultingPoxBehavior уже наследуется от WebHttpBehavior.
2) Если используются extensions в конфиге, то отпадает необходимость в [ErrorHandlerBehavior(typeof(CatalogErrorHandler))] на сервисе для обработки ошибок.
Есть ASP.NET приложение с Claims авторизацией.
ОтветитьУдалитьПрив входе вылетает такая вот ошибка, которую я никак не могу обойти
"Серверу не удалось обработать запрос из-за внутренней ошибки. Для получения дополнительной информации об ошибке включите IncludeExceptionDetailInFaults (или с помощью атрибута ServiceBehaviorAttribute, или из конфигурации поведения ) на сервере с целью отправки информации об исключении клиенту, либо включите трассировку, согласно документации Microsoft .NET Framework 3.0 SDK, и изучите журналы трассировки сервера."
Вопрос больше, как включить отображение ошибок.
В общем случае, показ информации об ошибке в wcf включается через web.config:
ОтветитьУдалитьserviceDebug includeExceptionDetailInFaults="true"
У меня вообще-то не wcf, а SharePoint, но думаю, что отладка должна быть аналогичной.
ОтветитьУдалитьХотелось бы подробней узнать, что именно прописывать в конфигурационном файле?
Такое вот решение от Microsoft мне не помогло (http://msdn.microsoft.com/ru-ru/library/system.servicemodel.servicebehaviorattribute.includeexceptiondetailinfaults.aspx):
С SharePoint я не помогу, к сожалению. А для того что бы понять как ловить ошибки в WCF, достаточно выполнить первую треть моего поста.
ОтветитьУдалитьСпасибо, уже разобрался. :) Оказалось, нужно было редактировать другой web.config: \14\WebServices\SecurityToken.
ОтветитьУдалитьДля wcf 4 все стало проще, там появился класс WebFaultException
ОтветитьУдалить