пятница, 25 декабря 2009 г.

Логирование сообщений в WCF

Работая с WCF редко приходится задумываться "А в каком виде сообщения передаются между клиентом и сервером". В общем случае, достаточно настроить web.config, что бы изменить протокол передачи данных.

Иногда для диагностических целей или для того что бы показать кому то пример сообщения, нужно эти сообщения увидеть. Для реализации этой задачи есть два способа.

1. Воспользоваться приемами трассировки.
2. Реализовать заказное поведение оконечной точки.

Трассировка и протоколирование сообщений в WCF

Трассировка используется для вывода информации о потоке выполнения и отдельных действий различных компонентов распределенного приложения. Механизм же протоколирования сообщений предназначен для сохранения содержимого сообщений, которыми обмениваются клиент и служба.
Протоколирование сообщений основано на классах из пространства имен System.Diagnostics и по умолчанию выключено. Что бы его включить, необходимо сначала добавить прослушиватель трассировки (например, XmlWriterTraceListener) для обработки информации от источника трассировки System.ServiceModel.MessageLogging.

Давайте изменим App.config клиента.
<configuration>
    <system.serviceModel>
        <behaviors .../>
        <diagnostics>
            <messageLogging 
                logEntireMessage="true" 
                logMessagesAtTransportLevel="true"
                maxMessagesToLog="4000" />

        </diagnostics>
    </system.serviceModel>

<system.diagnostics>
        <sources>
            <source name="System.ServiceModel.MessageLogging">
                <listeners>
                    <add type="System.Diagnostics.XmlWriterTraceListener" name="messages" initializeData="messages.svclog">
                    </add>
                </listeners>
            </source>

        </sources>
    </system.diagnostics>
</configuration>

Мы добавили источник System.ServiceModel.MessageLogging, с помощью которого выводятся сообщения для протоколирования. Обрабатываем же получаемую от него информацию прослушивателем XmlWriterTraceListener.
Теперь в файл messages.svclog пишутся логи сообщений. Для чтения и анализа этого файла можно воспользоваться программой Service Trace Viewer. Она может использоваться для импорта протоколов сообщений, сформированных одним или несколькими компонентами распределенного приложения. Эта программа входит в пакет Windows SDK.

Реализация заказного поведения для трассировки.

Заказные поведения позволяют вставить код в стратегически важные места. на этапе инициализации исполняющей среды, а также в контейнер обработки сообщений.

Реализация заказного поведения состоит из трех шагов:
Шаг 1. Создать класс, который реализует интерфейс Inspector, Selector, Formater или Invocer. В нашем случае это IClientMessageInspector

internal class LogMessageInspector : IClientMessageInspector
    {
        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            Console.WriteLine("---request---");
            Console.WriteLine(request.ToString());
            Console.WriteLine("---end---");
            return request;
        }

        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            Console.WriteLine("---Reply---");
            Console.WriteLine(reply.ToString());
            Console.WriteLine("---end---");
        }
    }

Шаг 2. создать класс, который реализует один из интерфейсов поведения: IServiceBehavior, IEndpointBehavior, IOperationBehavior или IContractBehavior. У нас это IEndpointBehavior.

public class LogEndpointBehavior : IEndpointBehavior
    {
        public void Validate(ServiceEndpoint endpoint)
        {
        }

        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new LogMessageInspector());
        }
    }

Шаг 3. Сконфигурировать клиент или службу, так чтобы они использовали созданное поведение. Это можно сделать в коде, в конфигурационом файле или с помощью атрибутов.
Я сделал это в коде клиента, при создании экземляра прокси.

            ChannelFactory<IFilms> factory = new ChannelFactory<IFilms>("poxWcfService");
            //Add logging of message to console
            factory.Endpoint.Behaviors.Add(new LogEndpointBehavior());
            IFilms client = factory.CreateChannel();

Собственно все, можно запускать консольный клиент и смотреть, а что же и в каком виде мы передаем на сервис.

понедельник, 21 декабря 2009 г.

Опыт использования Microsoft Security Essentials

Месяц назад попробовал поставить дома Wondows 7 64bit, в качестве антивируса решил попробовать Security Essentials от Микрософта, благо он тоже зарелизился да и бесплатный.


Одновременно сменил интернет провайдера и роутер.
Провайдером стал Триолан, скорость 100Мбит всего за 90 грн.
Роутер поставил гигабитный Linksys WRT320N

В результате HD рипы 1080p, по 8 гиг качаются всего за 15 минут из интернета, в общем счастье наступило.


Но, через некоторое время стала временами отваливаться сеть, запускаю диагностику сети, выполняется сброс сетевого адаптера и сеть поднимается. Долго думал, кто виноват, сетевая, роутер или провайдер.


Похоже виноватым оказался антивирус от Микрософт - Security Essentials.
Как то копировал 40 гигабайтный файл на внешний винчестер, но он что то очень медленно  читался, начал искать виноватого, оказалось антивирус съел всю память, пытаясь проанализировать этот файл.


Снес антивирус от Микрософта, поставил nod32 и о чудо сеть больше не падает и память не кончается.


Вот такой антивирус написан Микрософт, что и сеть от него падает и файлы большие не скопировать.


вторник, 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