ejemplos ejemplo await async c# reflection aop async-await castle-dynamicproxy

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; } }

  1. 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 la Task<T> a la Task normal

  2. Tenga en cuenta que estamos llamando a Task.ContinueWith() y no a Task<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.

Castle.Core.AsyncInterceptor

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.