вторник, 27 октября 2009 г.

Обработка ошибок в WCF при использовании REST подхода.


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>

Дело сделано, теперь и при REST подходе мы можем использовать единый механизм обработки ошибок WCF.

Исходный код проекта можно скачать по ссылке:

http://sites.google.com/site/mrdragonden/primery/ReliableService.rar?attredirects=0&d=1