使用动态代理截取异步方法的调用

33

以下是实现了Castle Dynamic Proxy库的IInterceptor接口自定义类型中的Intercept方法代码。这段代码取自一个基于面向切面编程(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;
        }
    }

我们发现在常规方法调用中,这个功能按预期工作,但是在使用C#5.0的async / await关键字的async方法中并不起作用。我相信我也理解了其中的原因。

为了让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...");

这仅适用于返回TaskTask<>而不是voidasync方法,但我对此很满意。

Intercept方法中需要做哪些更改,以使awaiter返回到那里而不是原始调用者?

8个回答

21

假设“问题”是它仅记录返回任务,并且你想要该任务中的

如果是这种情况,你仍然必须立即将任务返回给调用者-而不必等待其完成。如果违反此规则,会导致基本事物出现问题。

但是,在将任务返回给调用者之前,你应该添加一个续接(通过Task.ContinueWith),该续接将在任务完成时记录结果(或失败)。这仍将提供结果信息,但当然可以在其他某些日志记录之后记录它。你也可能要在返回之前立即记录,从而得到类似以下的日志:

Called FooAsync
Returned from FooAsync with a task
Task from FooAsync completed, with return value 5

如果任务成功完成,获取任务结果的业务将需要使用反射进行处理,这有点麻烦 - 或者您可以使用动态类型。 (无论哪种方式都会有一定的性能损失。)


谢谢Jon,我确实在寻找ContinueWith。是的,我希望拦截器继续记录返回值和任何异常。 - Hari Pachuveetil

20

多亏了Jon的答案,这就是我最终得出的结果:

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

6
谢谢您分享实施解决方案,了解不仅是Jon Skeet提供的指导,而且还知道您如何运用它。 - myermian
6
有趣的是:http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.asyncstatemachineattribute.aspx -- 可以使用此属性来检查方法是否标记为异步,而无需依靠返回类型是否为 TaskTask<T>... - myermian

10

尝试提供一种通用和清晰的解决方案:

  • 截取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;
}

4
以下是我的异步拦截器适配器实现,它可以正确处理异步方法。
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;
    }
}

4

我的两分钱意见:

已经正确确定,对于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)
{
    ...
}
  1. 但是,当处理async Task<T>方法时,上述方法会错误地更改截获返回的任务类型,从Task<T>更改为常规的Task

  2. 请注意,我们调用的是Task.ContinueWith()而不是我们想要调用的Task<TResult>.ContinueWith()方法。

当最终等待此类截取时,将出现以下异常:

System.InvalidCastException:无法将类型为'System.Threading.Tasks.ContinuationTaskFromTask'的对象强制转换为类型'System.Threading.Tasks.Task`1


2
嗨,我正在尝试实现一个可以处理返回Task<T>结果的方法的解决方案,但问题在于我无法动态地将其强制转换为适当的类型。您知道如何解决吗?谢谢。 - Anatoliy
修改返回值是一个错误。去掉invocation.ReturnValue = 并在任务上添加一个ContinueWith就可以使它在Task和Task<T>上正常工作,因为你只是在两个任务上添加了一个continueWIth,而不是试图设置原始ReturnValue,当你试图将它设置为Task时,当它是Task<T>时,这当然会出错。 - OJay

3

我需要拦截返回 Task<TResult> 的方法,因此我创建了一个扩展程序来简化这个过程,它基于 Castle.Core

Castle.Core.AsyncInterceptor 这个包可以在 NuGet 上下载。

这个解决方案主要是基于 silas-reinagel@silas-reinagel 的答案,但通过提供一个新的接口来实现 IAsyncInterceptor 来简化它。还有更多的抽象使拦截类似于实现 Interceptor

请参阅该项目的 readme 以获取更多详细信息。


0
   void IInterceptor.Intercept(IInvocation invocation) {
       try {
           invocation.Proceed();
           var task = invocation.ReturnValue as Task;
           if (task != null && task.IsFaulted) throw task.Exception;
       }
       catch {
           throw;
       }
   }

-1

改为:

tcs2.SetException(x.Exception);

你应该使用:

x.Exception.Handle(ex => { tcs2.SetException(ex); return true; });

将真正的异常冒泡...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接