В статье речь пойдет о крошечном усовершенствовании проекта Fody.MethodDecorator с добавлением возможности декорирования асинхронных методов.

Небольшое предисловие


В узких кругах широко известны такие инструменты аспектно-ориентированного программирования, как PostSharp и Fody.

Первый является условно-бесплатной утилитой и, на мой взгляд, крайне ограничен в своей бесплатной версии, в частности, его нельзя применить к проектам Windows Store, использовать автоматические INotifyPropertyChanged более чем в 10 классах, и так далее. Многие из этих ограничений и относительно высокая цена заставляют смотреть в сторону альтернатив.

Fody же, в свою очередь бесплатен, основан на Mono.Cecil и снабжен множеством плагинов. Более подробно о них можно прочитать в этой статье пользователя AlexeySuvorov. С одним из этих плагинов — MethodDecorator — тоже немного усовершенствованным автором предыдущей статьи — я столкнулся во время реализации логгирования.

Итак, декорирование методов


После загрузки пакета MethodDecoratorEx для упрощения логгирования (или еще какой-нибудь обработки) создается необходимый атрибут, который наследует интерфейс IMethodDecorator с методами входа, выхода и обработки исключения и навешивается на необходимые методы.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]
public class AsyncInterceptorAttribute : Attribute, IMethodDecorator
{
    public void Init(object instance, MethodBase methodBase, object[] args) { ... }

    public void OnEntry() { ... }
    public void OnExit() { ... }
    public void OnException(Exception exception) { ... }
}

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

[AsyncInterceptor]
public string Bar()
{
    return "Hi";
}



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

[AsyncInterceptor]
public async Task<string> Bar()
{
    throw new Exception();
}



Для решения этой проблемы был использован следующий обходной путь — модифицировать MethodDecoratorEx таким образом, чтобы можно было перехватить возвращаемый Task, и обработать его методом TaskContinuation следующим образом:

public void TaskContinuation(Task task)
{
    task.ContinueWith(OnTaskFaulted, TaskContinuationOptions.OnlyOnFaulted);
    task.ContinueWith(OnTaskCancelled, TaskContinuationOptions.OnlyOnCanceled);
    task.ContinueWith(OnTaskCompleted, TaskContinuationOptions.OnlyOnRanToCompletion);
}

private void OnTaskFaulted(Task t) { ... }
private void OnTaskCancelled(Task t) { ... }
private void OnTaskCompleted(Task t) { ... }



Что изменилось в проекте MethodDecoratorEx?


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

private static IEnumerable<Instruction> GetTaskContinuationInstructions(
    ILProcessor processor, 
    VariableDefinition retvalVariableDefinition,
    VariableDefinition attributeVariableDefinition,
    MethodReference taskContinuationMethodReference)
{
    if (retvalVariableDefinition == null) return new Instruction[0];
    var tr = retvalVariableDefinition.VariableType;

    if (tr.FullName.Contains("Task"))
    {
        return new[]
        {
            processor.Create(OpCodes.Ldloc_S, attributeVariableDefinition),
            processor.Create(OpCodes.Ldloc_S, retvalVariableDefinition),
            processor.Create(OpCodes.Callvirt, taskContinuationMethodReference),
        };
    }

    return new Instruction[0];
}


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

В частности, все xUnit тесты, которые были написаны для MethodDecoratorEx, теперь внезапно падают. Разбираться с этим пока нет времени, поэтому если у кого-то возникнет желание переписать тесты корректным образом, или помочь, буду рад.

Также можно немного усовершенствовать проверку на Task.

Проект тут
NuGet пакет тут

Большое спасибо за внимание.

Комментарии (9)


  1. AlexeySuvorov
    24.07.2015 19:13
    +2

    Будет минутка запилите пожалуйста pull request в github.com/alexeysuvorov/MethodDecorator я при возможности вмержу в оригинальный fody. Спасибо за фикс.


    1. NeoNN Автор
      25.07.2015 00:08
      +1

      Да, конечно. Так будет даже лучше.


  1. lair
    24.07.2015 23:51

    А как это будет работать, если у вас внутри async-метода несколько await?


    1. NeoNN Автор
      25.07.2015 00:08

      Точно такой же результат. Вход в метод, выход, continuations. А вы что имели в виду?


      1. lair
        25.07.2015 00:20

        Эмм. Метод с несколькими await, насколько я помню, сам уже представляет собой цепочку continuation. На какой из них вешаются ваши?


      1. lair
        25.07.2015 00:27

        Хотя наверное, я не прав, и внутри машинки не цепочка continuation, поэтому там не должно быть проблем. Вы проверяли поведение на сложном методе? (меня в основном исключения интересуют)


        1. mayorovp
          26.07.2015 17:17
          +1

          «Наши» продолжения вешаются на ту задачу, которую вернул нам метод.


  1. lair
    25.07.2015 00:21

    Второй вопрос: я же правильно понимаю, что ваши continuations никем не ожидаются, их выполнение не гарантировано, и exceptions никем не наблюдаются?


    1. NeoNN Автор
      25.07.2015 11:00

      Перехватываем task непосредственно перед возвратом из метода, если task faulted, то обрабатываем aggregate exception в соответствующем методе, если несколько await в методе — и они все бросают исключения — то вываливаемся на первом. Continuations лично для меня было проще сделать так, как есть, в атрибуте, а не встраивать непосредственно в IL, если встраивать — то получается много кода, на это не было времени. Если не перехватить в атрибуте — да, мы не будем наблюдать exception. Я вот не понимаю, если честно, в чем сложность подключить пакет и посмотреть, как он работает, если будут найдены ошибки, или предложен лучший вариант обработки, и я и остальные будут только благодарны. Вот это pastebin.com/0SMvYrrR работает на данный момент.