Улучшаем Fody MethodDecoratorEx для асинхронных методов

В статье речь пойдет о крошечном усовершенствовании проекта 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";
}


f52508ed916c4b668b3cb95c5ac43420.PNG

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

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


31e986fbff2a4af3ac002572cd51b099.PNG

Для решения этой проблемы был использован следующий обходной путь — модифицировать 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) { ... }


13ffd9ac8518445b86f4c70e2bc6e131.PNG

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


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

private static IEnumerable 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 пакет тут

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

© Habrahabr.ru