пятница, 18 октября 2013 г.

Использование перехватчиков Unity для формирования сбоев WCF

Привет! Давайте поговорим об использовании IoC-контейнера Unity для формирования типизированных сбоев (Fault) в WCF-службе.

Часто можно встретить код, который сочетает в себе как бизнес-логику, так и дополнительные инфраструктурные действия (логирование, обработку ошибок, создание коммуникационных сбоев). Написание такого кода является плохой практикой, так как затрудняет понимание и сопровождение кода. Для небольших проектов это не является большой проблемой, но в больших проектах, которые ведут больше одного человека, такой смешанный код будет категорически сложно сопровождать. Хорошим тоном считается вынесение инфраструктурного кода в отдельные методы и классы, создание из них декораторов для классов с бизнес-логикой.

Мне интересно рассмотреть этот обширный вопрос со стороны программирования служб WCF. Часто при создании служб программисты объединяют в одних методах логику и генерирование исключений и сбоев WCF. Давайте разберемся что есть что?

Исключение - это наследники класса Exception, которые являются стандартным механизмом оповещения о возникновения нештатной ситуации в .Net. Хорошим тоном считается оповещать вызывающую сторону о неполадках с помощью исключений. (Сразу оговорюсь, чтобы не обрушить на себя гнев читателей, что не надо выкидывать все исключения на вызывающую сторону. Какие исключения должны выходить во вне, а какие должны быть спрятаны от пользователя, должно решать во время проектирования приложения. Это важная задача, которая требует внимательного рассмотрения)

Особенность служб WCF в том, что любые исключения в методах службы не будут переданы за пределы самой службы. Вызывающий клиент получит стандартное исключение FaultException, в котором не будет приведено деталей возникшего исключения. Поэтому для  оповещения клиента WCF о сбое, имеется класс FaultException<T>, который является специализацией класса FaultException. Параметр типа T содержит всю необходимую информацию об ошибке. T может быть любым типом, но этот тип должен быть сериализуемый или являться контрактом данных. Клиент, получив экземпляр FaultException<T>, сможет обработать ошибку по своему усмотрению.

Обратите внимание, что FaultException<T> мы можем использовать для оповещения клиента об ошибках в нашей бизнес-логике, как, например, неверный ввод или отсутствие необходимых данных, и.т.д. При этом сбои FaultException<T> являются ошибками уровня коммуникации, а не бизнес-логики. Ни для кого не секрет, что эти уровни надо отделять друг от друга. Иначе мы получим код на подобие такого, как представлен ниже. Данный код мало читабельный, и он нарушает Принцип Единой Ответственности (SRP). Класс, отвечающий за работу бизнес-логики, не должен заботиться о создании коммуникационных сбоев.
public Guid PostReportToQueue(long id, List<ReportParams> parameters)
{
    IReport report = _repository.Find(id);
    if (report == null)
    {
        // Проверка и формирование сбоя уровня коммуникации.
        var fault = new ReportFault { Id = id, Message = @"Report doesn\'t exists." };
        throw new FaultException<ReportFault>(fault);
    }

    try
    {
        CheckParams(report, parameters);
    }
    catch (ParametersValidationException exception)
    {
        //Перехватываем исключение и преобразуем в нужный сбой
        var fault = new ParametersFault { Message = exception.Message };
        throw new FaultException<ParametersFault>(fault);
  
        //Что делать, если метод выбрасывает не одно исключение?
    }
    return _queue.Prepare(report, parameters);
}

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

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

Наша служба будет выполнять две задачи: ставить в очереди на подготовку отчет и проверять готовность отчета. Интерфейс службы приведен ниже:

[ServiceContract]
public interface IReportService
{
    [OperationContract]
    [FaultContract(typeof(ParametersFault))]
    [FaultContract(typeof(ReportFault))]
    Guid PostReportToQueue(long id, List<ReportParameter> parameters);

    [OperationContract]
    [FaultContract(typeof(ReportFault))]
    bool CheckReportIsReady(Guid reportId);
}

Метод PostReportToQueue принимает на вход идентификатор отчета, который хранится во внешнем хранилище, и список параметров, а возвращает уникальный идентификатор подготавливаемого экземпляра отчета, по которому позже можно проверить готовность отчета. Метод CheckReportIsReady по id отчета проверяет его готовность.

Оба метода могут генерировать два типа сбоев:
  • ParametersFault, если список параметров был некорректно задан;
  • ReportFault, в случае отсутствия макета отчета во внешнем хранилище или отчета в очереди на подготовку.
Сами методы ничего не знают о формируемых сбоях, методы оперируют обычными исключениями. 
public Guid PostReportToQueue(long id, List<ReportParameter> parameters)
{
    IReport report = _repository.Find(id);
    if (report == null)
    {
        throw new ReportNotExistsException(id, "Отчет не найден во внешнем хранилище.");
    }

    // Метод выбрасывает исключение ParametersValidationException 
    // при передаче некорректных параметров отчета.
    CheckParams(report, parameters);

    return _queue.EnqueuePrepare(report, parameters);
}

public bool CheckReportIsReady(Guid reportId)
{
    return _queue.CheckReady(reportId);
}

Из кода видим, что сами методы службы  выбрасывают исключения. Также исключения могут быть сгенерированы методами хранилища отчетов (_repository) и очереди подготовки (_queue). Всего в качестве примера у нас будет два типа исключений, в реальном проекте их число может составлять несколько десятков. Если оставить методы службы как есть, то при возникновении сбоя, клиент будет получать исключение FaultException. Никакой конкретной причины сбоя клиент не узнает.

Создадим класс, который будет  выступать в качестве декоратора для класса нашей службы. Он будет вызывать методы службы, принимать ответы от них, и, если возникло какое-либо исключение, будет конвертировать это исключение в соответствующий сбой. (Заметьте, что речь идет только о явно определенных бизнес-исключения, о которых мы знаем. Все незнакомые нам исключения преобразованы в сбои не будут.) Чтобы не писать много рукописного кода, воспользуемся механизмом перехватчиков Unity. Этот механизм позволяет определить специальный обработчик, который будет принимать делегат вызываемого метода службы, проверять его ответ и генерировать сбои при необходимости.

Основой механизмов перехватчиков является интерфейс ICallHandler. Не буду вдаваться в подробности его описание. Только скажу, что он состоит из метода Invoke, который производит вызов метода, а потом может выполнить необходимые действия с полученным от него результатом. А также их свойства Order, которое определяет порядок обработчика в цепочке. Подробней этот механизм можно изучить в документации Unity.

Создадим класс FaultInterceptor, реализующий интерфейс ICallHandler.

public class FaultInterceptor : ICallHandler
{
    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        IMethodReturn result = getNext()(input, getNext);
        Exception e = result.Exception;
        if (e == null)
            return result;

        //Любые контрактные исключения пропускаем 
        if (e is FaultException)
            return result;

        var paramException = e as ParametersValidationException;
        if (paramException != null)
        {
            var fault = new ParametersFault 
       {
           Message = paramException.Message
       };
            var exception = new FaultException<ParametersFault>(fault);
            return input.CreateExceptionMethodReturn(exception);
        }

        var reportException = e as ReportNotExistsException;
        if (reportException != null)
        {
            var fault = new ReportFault
                {
                    Message = reportException.Message,
                    Id = reportException.Id
                };
            var exception = new FaultException<ReportFault>(fault);
            return input.CreateExceptionMethodReturn(exception);
        }
        return result;
     }

    public int Order { get; set; }
}

Метод Invoke сначала производит вызов основного метода, используя свои магические параметры. Переменная result представляет собой результат выполнения метода. Если в результате выполнения метода возникло исключение, то result.Exception не будет равен null. Это как раз тот случай, который мы хотим обработать. Мы проверяем, что полученное исключение соответствует тем, которые мы ожидаем получить. Если это ожидаемые исключения, то мы подменяем их сбоями WCF и с помощью метода CreateExceptionMethodReturn подменяем результирующее исключение. Вызывающий метод получит новое исключение - наш сбой. Если же это не наше исключение, то просто вернем исходный результат.

Все, перехватчик готов. Теперь его необходимо зарегистрировать в DI контейнере для того, чтобы он оборачивал экземпляр нашей службы. Для этого необходимо и перехватчик, и службу зарегистрировать в DI контейнере, и в дальнейшем он сам все сделает. Это делается следующим кодом.


// В начале программы (см. исходники)
_container = new UnityContainer();
_container.AddNewExtension<Interception>(); // добавить обработку перехватчиков

_container.Configure<Interception>() // конфигурируем перехватчики
          .AddPolicy(PolicyName) // добавляем новую политику
          .AddMatchingRule<AlwaysMatchingRule>() // правило опредения что перехватывать
          .AddCallHandler<FaultInterceptor>(); // наш перехватчик

_container.RegisterType<IReportService, ReportService>(
    new TransientLifetimeManager(),
    new Interceptor<VirtualMethodInterceptor>(),
    new InterceptionBehavior<PolicyInjectionBehavior>(PolicyName));

Перед создание службы мы создаем наш контейнер, добавляем в него расширение для обработки перехватчиков. Настраиваем перехватчик, добавляя новую политику PolicyName, которая будет перехватывать вызов всех методов и пропускать их через класс нашего перехватчика FaultInterceptor.

Далее регистрируем тип службы в контейнере так, чтобы к нему применялась наша политика перехвата. Опустив подробности, которые можно почерпнуть в документации Unity и исходниках примера, скажу, что остается два важных момента:

  • пометить все методы службы как виртуальные, так как выше мы настроили перехватчик виртуальных методов. Без этого Unity может не сработать.
  • В атрибуте ServiceBehavior службы добавить параметр ConfigurationName = "ReportingService.ReportService". Если этого не  сделать, то при регистрации экземпляра службы в хосте мы получим исключение. Это происходит из-за того, что Unity создаст прокси класс - обертку над службой, и ее попробует зарегистрировать в хосте. Но для этого прокси класса не будет найдены параметры конфигурации службы. А ConfigurationName  явно прописывает какие конфигурации необходимо использовать.
На этом все. 

Основной моей целью не было подробно рассказать про сбои, перехватчики, DI, и принципы проектирования. Я хотел просто привести небольшой пример, как можно сочетать различные технологии и принципы для решения конкретной задачи. 

Исходники примера лежат тут.

1 комментарий: