Перехватить вызов асинхронного метода с помощью DynamicProxy
Ниже приведен код из Intercept
метод пользовательского типа, который реализует IInterceptor
библиотеки динамического прокси замка. Этот фрагмент взят из консольного приложения, основанного на протоколе AOP, которое размещено здесь.
public void Intercept(IInvocation invocation)
{
if (Log.IsDebugEnabled) Log.Debug(CreateInvocationLogString("Called", invocation));
try
{
invocation.Proceed();
if (Log.IsDebugEnabled)
if (invocation.Method.ReturnType != typeof(void))
Log.Debug("Returning with: " + invocation.ReturnValue);
}
catch (Exception ex)
{
if (Log.IsErrorEnabled) Log.Error(CreateInvocationLogString("ERROR", invocation), ex);
throw;
}
}
Это работает, как и ожидалось, при обычных вызовах методов, но не при попытке с async
методы (с использованием async/await
ключевые слова из C# 5.0). И я верю, я понимаю причины этого также.
Для async/await
для работы компилятор добавляет функциональное тело метода в конечный автомат за кулисами, и элемент управления возвращается к вызывающей стороне, как только первый awaitable
Выражение, которое не может быть завершено синхронно, встречается.
Кроме того, мы можем запросить тип возвращаемого значения и выяснить, имеем ли мы дело с async
метод как это:
if (invocation.Method.ReturnType == typeof(Task) ||
(invocation.Method.ReturnType.IsGenericType &&
invocation.Method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
Log.Info("Asynchronous method found...");
Это работает только для тех, async
методы, которые возвращают либо Task
или же Task<>
и не void
но я в порядке с этим.
Какие изменения должны быть сделаны в рамках Intercept
метод, так что awaiter
вернется туда, а не оригинальный абонент?
8 ответов
Предположительно, "проблема" в том, что он просто регистрирует, что возвращает задачу - и вам нужно значение в этой задаче?
Если предположить, что это так, вам все равно придется немедленно вернуть задачу вызывающей стороне, не дожидаясь ее завершения. Если вы сломаете это, вы в корне запутаетесь.
Однако, прежде чем вернуть задачу вызывающей стороне, следует добавить продолжение (через Task.ContinueWith
) который будет регистрировать результат (или неудачу), когда задача завершится. Это все равно даст информацию о результате, но, конечно, вы будете регистрировать ее потенциально после некоторой другой регистрации. Вы также можете захотеть войти непосредственно перед возвращением, что приведет к записи чего-то вроде этого:
Called FooAsync
Returned from FooAsync with a task
Task from FooAsync completed, with return value 5
Бизнес по получению результата из задачи (если он успешно завершен) должен был быть выполнен с помощью рефлексии, что немного болезненно - или вы могли бы использовать динамическую типизацию. (В любом случае это будет немного удар по производительности.)
Благодаря ответу Джона, это то, чем я закончил:
public void Intercept(IInvocation invocation)
{
if (Log.IsDebugEnabled) Log.Debug(CreateInvocationLogString("Called", invocation));
try
{
invocation.Proceed();
if (Log.IsDebugEnabled)
{
var returnType = invocation.Method.ReturnType;
if (returnType != typeof(void))
{
var returnValue = invocation.ReturnValue;
if (returnType == typeof(Task))
{
Log.Debug("Returning with a task.");
}
else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
Log.Debug("Returning with a generic task.");
var task = (Task)returnValue;
task.ContinueWith((antecedent) =>
{
var taskDescriptor = CreateInvocationLogString("Task from", invocation);
var result =
antecedent.GetType()
.GetProperty("Result")
.GetValue(antecedent, null);
Log.Debug(taskDescriptor + " returning with: " + result);
});
}
else
{
Log.Debug("Returning with: " + returnValue);
}
}
}
}
catch (Exception ex)
{
if (Log.IsErrorEnabled) Log.Error(CreateInvocationLogString("ERROR", invocation), ex);
throw;
}
}
Попытка уточнить с помощью общего и чистого решения для:
- Перехватив
async
методы добавления пользовательского кода в качестве продолжения задачи.
Я думаю, что лучшим решением является использование dynamic
ключевое слово для обхода проверки типа компилятора и устранения различий между Task и Task<T>
во время выполнения:
public void Intercept(IInvocation invocation)
{
invocation.Proceed();
var method = invocation.MethodInvocationTarget;
var isAsync = method.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null;
if (isAsync && typeof(Task).IsAssignableFrom(method.ReturnType))
{
invocation.ReturnValue = InterceptAsync((dynamic)invocation.ReturnValue);
}
}
private static async Task InterceptAsync(Task task)
{
await task.ConfigureAwait(false);
// do the logging here, as continuation work for Task...
}
private static async Task<T> InterceptAsync<T>(Task<T> task)
{
T result = await task.ConfigureAwait(false);
// do the logging here, as continuation work for Task<T>...
return result;
}
Мои 2 цента:
Было правильно установлено, что для async
Методы Цель перехватчика состоит в том, чтобы "улучшить" задачу, возвращаемую вызовом, через продолжение.
Теперь, именно это продолжение задачи должно быть возвращено, чтобы завершить работу перехватчика.
Таким образом, основываясь на приведенных выше обсуждениях и примерах, это будет прекрасно работать как для обычных, так и для "сырых" методов. async Task
методы.
public virtual void Intercept(IInvocation invocation)
{
try
{
invocation.Proceed();
var task = invocation.ReturnValue as Task;
if (task != null)
{
invocation.ReturnValue = task.ContinueWith(t => {
if (t.IsFaulted)
OnException(invocation, t.Exception);
});
}
}
catch (Exception ex)
{
OnException(invocation, ex);
}
}
public virtual void OnException(IInvocation invocation, Exception exception)
{
...
}
Но когда имеешь дело с
async Task<T>
Методы, приведенные выше неправильно изменили бы тип задачи, возвращаемой при перехвате, сTask<T>
регулярноTask
Обратите внимание, что мы звоним
Task.ContinueWith()
и неTask<TResult>.ContinueWith()
, который является методом, который мы хотим вызвать.
Это будет результирующее исключение, когда в конечном итоге ожидается такой перехват:
System.InvalidCastException: Невозможно привести объект типа 'System.Threading.Tasks.ContinuationTaskFromTask' к типу 'System.Threading.Tasks.Task`1.
Ниже моя реализация адаптера асинхронного перехватчика, которая правильно обрабатывает асинхронные методы.
abstract class AsyncInterceptor : IInterceptor
{
class TaskCompletionSourceMethodMarkerAttribute : Attribute
{
}
private static readonly MethodInfo _taskCompletionSourceMethod = typeof(AsyncInterceptor)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.Single(x => x.GetCustomAttributes(typeof(TaskCompletionSourceMethodMarkerAttribute)).Any());
protected virtual Task<Object> InterceptAsync(Object target, MethodBase method, object[] arguments, Func<Task<Object>> proceed)
{
return proceed();
}
protected virtual void Intercept(Object target, MethodBase method, object[] arguments, Action proceed)
{
proceed();
}
[TaskCompletionSourceMethodMarker]
Task<TResult> TaskCompletionSource<TResult>(IInvocation invocation)
{
var tcs = new TaskCompletionSource<TResult>();
var task = InterceptAsync(invocation.InvocationTarget, invocation.Method, invocation.Arguments, () =>
{
var task2 = (Task)invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments);
var tcs2 = new TaskCompletionSource<Object>();
task2.ContinueWith(x =>
{
if (x.IsFaulted)
{
tcs2.SetException(x.Exception);
return;
}
dynamic dynamicTask = task2;
Object result = dynamicTask.Result;
tcs2.SetResult(result);
});
return tcs2.Task;
});
task.ContinueWith(x =>
{
if (x.IsFaulted)
{
tcs.SetException(x.Exception);
return;
}
tcs.SetResult((TResult)x.Result);
});
return tcs.Task;
}
void IInterceptor.Intercept(IInvocation invocation)
{
if (!typeof(Task).IsAssignableFrom(invocation.Method.ReturnType))
{
Intercept(invocation.InvocationTarget, invocation.Method, invocation.Arguments, invocation.Proceed);
return;
}
var returnType = invocation.Method.ReturnType.IsGenericType ? invocation.Method.ReturnType.GetGenericArguments()[0] : typeof(object);
var method = _taskCompletionSourceMethod.MakeGenericMethod(returnType);
invocation.ReturnValue = method.Invoke(this, new object[] { invocation });
}
}
и образец использования:
class TestInterceptor : AsyncInterceptor
{
protected override async Task<Object> InterceptAsync(object target, MethodBase method, object[] arguments, Func<Task<object>> proceed)
{
await Task.Delay(5000);
var result = await proceed();
return DateTime.Now.Ticks % 2 == 0 ? 10000 :result;
}
}
Необходимость перехвата возвращаемых методов Task<TResult>
Я создал расширение для Castle.Core
это упрощает процесс.
https://github.com/JSkimming/Castle.Core.AsyncInterceptor
Пакет доступен для скачивания на NuGet.
Решение в значительной степени основано на этом ответе от Silas Reinagel, но упрощает его, предоставляя новый интерфейс для реализации IAsyncInterceptor. Есть также дополнительные абстракции, которые делают перехват похож на реализацию Interceptor
,
Смотрите readme проекта для более подробной информации.
void IInterceptor.Intercept(IInvocation invocation) { try { invocation.Proceed(); var task = invocation.ReturnValue as Task; if (task != null && task.IsFaulted) throw task.Exception; } catch { throw; } }
Вместо:
tcs2.SetException(x.Exception);
Вы должны использовать:
x.Exception.Handle(ex => { tcs2.SetException(ex); return true; });
вспыхнуть реальным исключением...