Улучшаем 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";
}
Но если метод асинхронный, возникает проблема. Вход в метод и выход из метода перехватываются без проблем, но вот исключения, если они возникают, не логгируются вовсе. Так происходит потому, что даже украшенный атрибутом асинхронный метод транслируется в особую, конечно-автоматную магию, что не позволяет перехватить исключение в нашем методе.
[AsyncInterceptor]
public async Task 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 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 пакет тут
Большое спасибо за внимание.