Pull to refresh

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

Reading time 3 min
Views 5.6K
В статье речь пойдет о крошечном усовершенствовании проекта 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 пакет тут

Большое спасибо за внимание.
Tags:
Hubs:
+7
Comments 9
Comments Comments 9

Articles