суббота, 20 декабря 2014 г.

Обработка ошибок в сервисах .svc

    В настоящий момент, я стараюсь как можно большую часть бизнес логики приложения переносить на клиента. Но иногда возникают сложности. Например, кто знал, что нельзя получить размер файла attachment’а SPItem’а, использую CSOM или REST. В таких моментах приходится звать на помощь серверный код, а именно .svc сервисы.

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

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class ListService : IListService
    {
        #region propery
        private SPContext _spcontext;
        private SPContext Context
        {
            get
            {
                _spcontext = _spcontext ?? SPContext.GetContext(HttpContext.Current);
                return _spcontext;
            }
        }
        private SPWeb _web;
        private SPWeb Web
        {
            get
            {
                _web = _web ?? Context.Web;
                return _web;
            }
        }
        #endregion
        public Guid GetListID(string title)
        {
            SPList list = Web.Lists.TryGetList(title);
            if (list == null)
            {
                throw new SPException("List not found");
            }
            return list.ID;
        }
    }


   И его интерфейс

    [ServiceContract]
    interface IListService
    {
        [OperationContract]
        [WebInvoke(Method = "GET", 
            UriTemplate = "GetListID/{title}",
            ResponseFormat = WebMessageFormat.Json,
           RequestFormat = WebMessageFormat.Json, 
            BodyStyle = WebMessageBodyStyle.Bare)]
        Guid GetListID(string title);
    }

   Ну вот результат результат его работы.
   Все работает. Так в чем же проблема? А что будет, если мы ошибемся в название списка? Давайте проверим.. 
    Мда.. Это не то, что я ожидал. Это сообщение не дает никакой информации об ошибки и как ее исправить. Надо менять сие поведение!

    Если вы работаете с .net 4 и выше – есть очень простой способ исправить это. Необходимо использовать класс WebFaultException вместо обычного класса исключения. Сейчас покажу как. Изменим метод GetLIstID нашего сервиса.

        public Guid GetListID(string title)
        {
            SPList list = Web.Lists.TryGetList(title);
            if (list == null)
            {
                throw new WebFaultException("List not found", HttpStatusCode.InternalServerError);
            }
            return list.ID;
        } 

    И проверим результат.
  
   Отлично, все работает, как нам надо. Правда, возник другой вопрос - а что будет если ошибка возникнет там, где мы ее не ожидаем? 
   Еще раз поменяем код. Попробуем с эмулировать эту ситуацию. Заменим SPList list = Web.Lists.TryGetList(title) на SPList list = Web.Lists[title]. Допустим мы не подозреваем, что теперь получим исключение, если имя списка не правильное. Смотрим результат.. Ну и предсказуемо мы видим «Request Error». 
   Как один из вариантов исправить это -  обернуть все операции(ну или весь метод сервиса), которые могут потенциально вызвать исключение, в блок try/catch и в catch выбрасывать WebFaultException. Но это лишний код и вообще как-то нехорошо, к тому же не стоит забывать этот класс доступен только для .net 4 и выше. 
   После быстрого поиска в гугле, попалась вот эта статья.  В двух словах для решения этой проблемы необходимо сделать следующее: 
  1. Создать класс реализующий интерфейс IErrorHandler, который будет обрабатывать ошибки.
  2. Создать наследника WebHttpBehavior  и переопределить стандартный обработчик ошибок, на созданный нами ранее.
  3. Создать наследника WebServiceHostFactory и добавить к нему поведение созданное в шаге 2.   
   Выглядит все просто. Ну что ж, начнем.
   Прежде всего нам необхожимо создать класс, который мы будем возвращать при ошибке.  Назовем его, например, CustomFault. 


[DataContract]
    class CustomFault
    {
        [DataMember]
        public string Message { get; set; }
    }

   Не забудьте добавить атрибуты [DataContract] к нему и [DataMember] к его свойствам.  В нем всего лишь одно свойство – сообщение, в котором будет информация об ошибке.   
   Хорошо, а теперь создадим наш собственный обработчик ошибок.
    

class ErrorHandler : IErrorHandler
    {
        public bool HandleError(Exception error)
        {
            return true;
        }

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            CustomFault ex = new CustomFault();
            ex.Message = error.Message;
            fault = Message.CreateMessage(MessageVersion.None, "",ex, new DataContractJsonSerializer(ex.GetType()));
            var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
            fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);
            WebOperationContext.Current.OutgoingResponse.ContentType = "application/json";
            WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError;
        }
    }


    Нам необходимо реализовать свойство HandleError, определяющее будем ли мы обрабатывать ошибки. Просто возвращаем true. Ну и метод ProvideFault где и обрабатываем ошибку и возвращаем JSON. Создаем CustomFault, инициализируем единственное свойство тестом исключения и добавляем наш объект к экземпляру класса Message, который и вернется клиенту. Также не забываем указать Content-Type и StatusCode ответа сервера.

    Создадим потомка WebHttpBehavior и добавим наш обработчик ошибок.


class WebHttpBehaviorEx : WebHttpBehavior
    {
       protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear();
            endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new ErrorHandler());
        }
    }


    Остался последний шаг и вот тут нас ожидают проблемы. Когда мы создали наш сервис, мы в файле .svc указали какой класс использовать как фабрику, а именно класс Microsoft.SharePoint.Client.Services.MultipleBaseAddressWebServiceHostFactory. Почему именно его и зачем он вообще нужен почитайте здесь. Нам надо изменить его поведение, а именно переопределить создание экземпляра класса ServiceHost . Как вариант, попробуем унаследоваться и переопределить эти методы. Сказано сделано.

class CustomFactory : MultipleBaseAddressWebServiceHostFactory
    {
        public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
        {
            var sh = new ServiceHost(typeof(ListService), baseAddresses);
            sh.Description.Endpoints[0].Behaviors.Add(new WebHttpBehaviorEx());
            return sh;
        }
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            return base.CreateServiceHost(serviceType, baseAddresses);
        }
    }


    И не забудем поменять наш svc файл, приведем его к виду


<%@ ServiceHost Language="C#" Debug="true"
    Service="CustomService.NVServices.ListService, $SharePoint.Project.AssemblyFullName$"
    CodeBehind="ListService.svc.cs"
    Factory="CustomService.NVServices.CustomFactory,
     $SharePoint.Project.AssemblyFullName$"%>



   Проверим, что получили.. Ошибка, но уже с CorrelationID, что радует. Находит в ее в логах - This collection already contains an address with scheme http. Ммм.. т.е сервис уже создан? Проверим, дебаг еще никто не отменял.
 
    И в действительности, не получается создать экземпляр объекта ServiceHost. Причина скорее всего в том, что CreateServiceHost(Type serviceType, Uri[] baseAddresses) отрабатывает раньше и base.CreateServiceHost(serviceType, baseAddresses) уже создал нам экземпляр ServiceHost. Придется капать глубже, а куда глубже, чем в исходники?) К тому же идея была не ахти какая. Что пришлось бы делать, если сервисов было несколько – создать для каждого класс фабрику? Ну нет уж. 
   Открываем ILSpy и посмотрим из чего состоит MultipleBaseAddressWebServiceHostFactory. Для этого нам понадобиться сборка Microsoft.SharePoint.Client.ServerRuntime.dll(C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI). Вот так вот выглядит этот класс.


   Мы пытались переопределить не тот метод, которы нам нужен и наша фабрика работает с наследником ServiceHost. Посмотрим, что из себя представляет  MultipleBaseAddressWebServiceHost.


   Вот и нашли место, где мы сможем добавить наш WebHttpBehaviorEx. А именно метод OnOpening класса MultipleBaseAddressWebServiceHost. Ну что ж сделаем это, создадим своего наследника класса MultipleBaseAddressWebServiceHost.

[ServiceFactoryUsingAuthSchemeInEndpointAddress]
    class VirtoMultipleHttpBindingServiceHost : MultipleBaseAddressWebServiceHost
    {
        public VirtoMultipleHttpBindingServiceHost(Type serviceType, params Uri[] baseAddresses)
            : base(serviceType, Utility.GetBaseAddressesWithUniqueScheme(baseAddresses))
        {

        }
        protected override void OnOpening()
        {
            base.OnOpening();
            Description.Endpoints[0].Behaviors.Add(new WebHttpBehaviorEx());
        }
    }


    Utility.GetBaseAddressesWithUniqueScheme(baseAddresses) - это копия метода ServiceUtility.GetBaseAddressesWithUniqueScheme(baseAddresses). К сожалению класс ServiceUtility является internal и мы не сможем им воспользоваться.

    И теперь осталось изменить наш класс CustomFactory.  


class CustomFactory : WebServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            return new VirtoMultipleHttpBindingServiceHost(serviceType, baseAddresses);
        }
    }


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

Комментариев нет:

Отправить комментарий