Небольшое предисловие
В узких кругах широко известны такие инструменты аспектно-ориентированного программирования, как 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)
lair
25.07.2015 00:21Второй вопрос: я же правильно понимаю, что ваши continuations никем не ожидаются, их выполнение не гарантировано, и exceptions никем не наблюдаются?
NeoNN Автор
25.07.2015 11:00Перехватываем task непосредственно перед возвратом из метода, если task faulted, то обрабатываем aggregate exception в соответствующем методе, если несколько await в методе — и они все бросают исключения — то вываливаемся на первом. Continuations лично для меня было проще сделать так, как есть, в атрибуте, а не встраивать непосредственно в IL, если встраивать — то получается много кода, на это не было времени. Если не перехватить в атрибуте — да, мы не будем наблюдать exception. Я вот не понимаю, если честно, в чем сложность подключить пакет и посмотреть, как он работает, если будут найдены ошибки, или предложен лучший вариант обработки, и я и остальные будут только благодарны. Вот это pastebin.com/0SMvYrrR работает на данный момент.
AlexeySuvorov
Будет минутка запилите пожалуйста pull request в github.com/alexeysuvorov/MethodDecorator я при возможности вмержу в оригинальный fody. Спасибо за фикс.
NeoNN Автор
Да, конечно. Так будет даже лучше.