c# - ejemplo - Intercepta la llamada a un método asíncrono usando DynamicProxy
async await c# ejemplos (8)
void IInterceptor.Intercept(IInvocation invocation) { try { invocation.Proceed(); var task = invocation.ReturnValue as Task; if (task != null && task.IsFaulted) throw task.Exception; } catch { throw; } }
A continuación se muestra el código del método de Intercept
en un tipo personalizado que implementa IInterceptor
de la biblioteca Castle Dynamic Proxy . Este fragmento es de una aplicación de consola de prueba de concepto de registro basada en AOP que se publica here .
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;
}
}
Esto funciona como se espera en las llamadas a métodos regulares, pero no cuando se intenta con métodos async
(usando las palabras clave async/await
de C # 5.0). Y creo, entiendo las razones detrás de esto también.
Para que async/await
funcione, el compilador agrega el cuerpo funcional del método a una máquina de estados detrás de escena y el control regresará al llamante, tan pronto como se encuentre la primera expresión que no se puede completar de forma sincronizada.
Además, podemos interrogar el tipo de retorno y averiguar si estamos tratando con un método async
como este:
if (invocation.Method.ReturnType == typeof(Task) ||
(invocation.Method.ReturnType.IsGenericType &&
invocation.Method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
Log.Info("Asynchronous method found...");
Esto funciona solo para aquellos métodos async
que devuelven una Task
o una Task<>
y no se void
pero estoy bien con eso.
¿Qué cambios se deben hacer dentro del método de Intercept
para que el awaiter
regrese allí en lugar del llamante original?
A continuación se muestra la implementación de mi adaptador de interceptor asíncrono que maneja correctamente los métodos asíncronos.
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)
{
...
}
y uso de la muestra:
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 });
}
}
En lugar de:
tcs2.SetException(x.Exception);
Deberías usar:
x.Exception.Handle(ex => { tcs2.SetException(ex); return true; });
para burbujear la verdadera excepción ...
Gracias a la respuesta de Jon, esto es con lo que terminé:
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;
}
}
Mis 2 centavos:
Se ha establecido correctamente que para los métodos async
el propósito del interceptor sería "mejorar" la tarea devuelta por la invocación, a través de una continuación.
Ahora, es precisamente esta continuación de la tarea la que debe devolverse para completar el trabajo del interceptor.
Por lo tanto, en base a las discusiones y ejemplos anteriores, esto funcionaría perfectamente bien para los métodos regulares así como para los métodos async Task
"sin procesar" de async Task
.
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;
}
}
Pero cuando se trata de métodos
async Task<T>
, lo anterior cambiaría incorrectamente el tipo de tarea devuelta por la intercepción, de laTask<T>
a laTask
normalTenga en cuenta que estamos llamando a
Task.ContinueWith()
y no aTask<TResult>.ContinueWith()
, que es el método al que queremos llamar.
Esta sería la excepción resultante cuando finalmente se espera la intercepción de este tipo:
System.InvalidCastException: no se puede convertir el objeto de tipo ''System.Threading.Tasks.ContinuationTaskFromTask'' para escribir ''System.Threading.Tasks.Task`1
Presumiblemente, el "problema" es que solo está registrando que está devolviendo una tarea, ¿y desea el valor dentro de esa tarea?
Suponiendo que ese sea el caso, todavía tiene que devolver la tarea a la persona que llama, inmediatamente, sin esperar a que se complete. Si rompes eso, básicamente estás arruinando las cosas.
Sin embargo, antes de devolver la tarea a la persona que llama, debe agregar una continuación (a través de Task.ContinueWith
) que registrará el resultado (o la falla) cuando la tarea se complete . Eso seguirá dando la información del resultado, pero, por supuesto, lo registrarás potencialmente después de algún otro registro. También puede querer iniciar sesión inmediatamente antes de regresar, lo que lleva a un registro como este:
Called FooAsync
Returned from FooAsync with a task
Task from FooAsync completed, with return value 5
El negocio de obtener el resultado de la tarea (si se completó con éxito) tendría que hacerse con la reflexión, lo cual es un poco molesto, o podría usar la escritura dinámica. (De cualquier manera, será un poco un éxito de rendimiento).
Tratando de aclarar con una solución genérica y limpia para:
- Interceptando métodos
async
agregando código personalizado como una tarea de continuación.
Creo que la mejor solución es usar la palabra clave dynamic
para omitir la comprobación de tipo de compilador y resolver la diferencia entre Tarea y Tarea <T>
en tiempo de ejecución:
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;
}
Task<TResult>
necesidad de interceptar métodos que devuelven la Task<TResult>
, he creado una extensión para Castle.Core
que simplifica el proceso.
El paquete está disponible para descargar en NuGet .
La solución se basa en gran medida en esta answer de @silas-reinagel , pero la simplifica al proporcionar una nueva interfaz para implementar IAsyncInterceptor . También hay abstracciones adicionales que hacen que la intercepción sea similar a la implementación de Interceptor
.
Ver el readme del proyecto para más detalles.