【问题标题】:Intercept the call to an async method using DynamicProxy使用 DynamicProxy 拦截对异步方法的调用
【发布时间】:2012-12-26 14:53:10
【问题描述】:

下面是实现Castle Dynamic Proxy 库的IInterceptor 的自定义类型上的Intercept 方法的代码。此 sn-p 来自基于 AOP 的日志记录概念验证控制台应用程序,该应用程序发布在 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;
        }
    }

这在常规方法调用中按预期工作,但在使用 async 方法(使用 C# 5.0 中的 async/await 关键字)尝试时却不行。我相信,我也理解这背后的原因。

为了使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&lt;&gt; 而不是 voidasync 方法,但我可以接受。

必须在Intercept 方法中进行哪些更改才能使awaiter 返回到那里而不是原来的调用者?

【问题讨论】:

    标签: c# reflection aop async-await castle-dynamicproxy


    【解决方案1】:

    由于需要拦截返回 Task&lt;TResult&gt; 的方法,我创建了 Castle.Core 的扩展来简化流程。

    Castle.Core.AsyncInterceptor

    该软件包可在NuGet 下载。

    该解决方案主要基于来自@silas-reinagelanswer,但通过提供一个新接口来实现IAsyncInterceptor 来简化它。还有一些进一步的抽象,使拦截类似于实现Interceptor

    更多详情请查看项目的readme

    【讨论】:

      【解决方案2】:

      我的 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)
      {
          ...
      }
      
      1. 但是在处理async Task&lt;T&gt;方法时,上面会错误的改变拦截返回的任务类型,从Task&lt;T&gt;变成普通的Task

      2. 请注意,我们调用的是Task.ContinueWith(),而不是Task&lt;TResult&gt;.ContinueWith(),这是我们要调用的方法。

      这将是最终等待此类拦截时产生的异常:

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

      【讨论】:

      • 嗨。我正在尝试使用返回 Task 结果的方法来实现正确的解决方案,问题是我无法动态转换为适当的类型。你知道怎么解决吗?谢谢。
      【解决方案3】:

      尝试使用通用且干净的解决方案来澄清:

      • 拦截async 方法添加自定义代码作为延续任务。

      我认为最好的解决方案是使用dynamic关键字绕过编译器类型检查,在运行时解决Task和Task&lt;T&gt;的区别:

      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】:

        代替:

        tcs2.SetException(x.Exception);
        

        你应该使用:

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

        冒出真正的例外......

        【讨论】:

          【解决方案5】:
             void IInterceptor.Intercept(IInvocation invocation) {
                 try {
                     invocation.Proceed();
                     var task = invocation.ReturnValue as Task;
                     if (task != null && task.IsFaulted) throw task.Exception;
                 }
                 catch {
                     throw;
                 }
             }
          

          【讨论】:

            【解决方案6】:

            下面是我正确处理异步方法的异步拦截器适配器实现。

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

            【讨论】:

              【解决方案7】:

              大概“问题”是它只是记录它正在返回一个任务 - 而您想要该任务中的

              假设是这种情况,您仍然必须立即将任务返回给调用者 - 无需等待它完成。如果你打破了这一点,你从根本上把事情搞砸了。

              但是,在将任务返回给调用者之前,您应该添加一个延续(通过Task.ContinueWith),它将在任务完成时记录结果(或失败)。这仍然会提供结果信息,但当然您可能会在其他一些日志记录之后记录它。您可能想在返回之前立即登录,导致日志如下所示:

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

              从任务中获取结果(如果成功完成)必须通过反射来完成,这有点麻烦 - 或者您可以使用动态类型。 (无论哪种方式,都会对性能造成一点影响。)

              【讨论】:

              • 谢谢乔恩,我肯定在找ContinueWith。是的,我希望拦截器继续记录返回值和其中的任何异常。
              【解决方案8】:

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

              【讨论】:

              • 感谢您分享实施的解决方案,很高兴不仅了解 Jon Skeet 提供的指导,还了解您如何接受并应用它。
              • 一些有趣的事情要注意:msdn.microsoft.com/en-us/library/… -- 可以探测该属性以确定方法是否标记为异步,而不是依赖于返回类型是 Task 或 @ 987654324@...
              • 谢谢@m-y...这确实有帮助:)
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-10-06
              • 2011-01-10
              • 2018-09-08
              • 2023-03-03
              • 1970-01-01
              • 2014-10-11
              相关资源
              最近更新 更多