异步编程

使用 Unity 拦截功能拦截异步方法

Fernando Simonazzi
Grigori Melnik

下载代码示例

Unity(不要与 Unity3D 游戏引擎相混淆)是一种具有拦截支持的可扩展通用依赖关系注入容器,可用于基于 Microsoft .NET Framework 的任何类型的应用程序。Unity 由 Microsoft 模式和实施方案组 (microsoft.com/practices) 设计和维护。可通过 NuGet 轻松将其添加到您的应用程序中,在 msdn.com/unity 上可以找到有关 Unity 的主要资源学习中心。

本文将着重介绍 Unity 拦截。拦截是您要修改单个对象的行为,同时不影响同一类中其他对象的行为时常用的一种方法,在使用 Decorator 模式时您会经常使用此方法(有关 Decorator 模式的维基百科定义,请访问: bit.ly/1gZZUQu)。拦截提供了在运行时将新行为添加到对象的灵活方法。这些行为一般涉及一些横切关注点,例如日志记录或数据验证。拦截通常用作面向方面的编程 (AOP) 的底层机制。Unity 中的运行时拦截功能可使您有效拦截对对象的方法调用,以及执行这些调用的前期和后期处理。

Unity 容器中的拦截有两个主要组件:拦截程序和拦截行为。拦截程序确定拦截被拦截对象中的方法调用所使用的机制,而拦截行为确定在被拦截方法调用上执行的操作。被拦截对象带有拦截行为的管道。当拦截方法调用时,将允许该管道中的每个行为检查甚至修改此方法调用的参数,并最终调用原始方法实现。在返回时,每个行为均可检查或替代返回的值,或者原始实现或管道中的先前行为引发的异常。最后,原始调用程序获得产生的返回值(如果有)或产生的异常。图 1 描述了此拦截机制。

Unity Interception Mechanism图 1 Unity 拦截机制

拦截方法有两种类型:实例拦截和类型拦截。凭借实例拦截,Unity 动态创建在客户端与目标对象之间插入的代理对象。然后,代理对象负责通过这些行为将客户端进行的调用传递到目标对象。您可以使用 Unity 实例拦截来拦截由 Unity 容器创建以及在该容器之外创建的对象,并且您可以使用它来拦截虚拟和非虚拟方法。但您无法将动态创建的代理类型转换为目标对象的类型。凭借类型拦截,Unity 动态创建从目标对象类型派生的新类型,并且此类型包含处理横切关注点的行为。Unity 容器在运行时对派生类型的对象进行实例化。实例拦截只可拦截公共实例方法。类型拦截可以拦截公共和受保护的虚拟方法。记住,由于平台限制,Unity 拦截不支持 Windows Phone 和 Windows 应用商店的应用开发,但核心 Unity 容器支持。

有关 Unity 的入门指导,请参阅“使用 Unity 进行依赖关系注入”(Microsoft 模式和实施方案,2013 年)amzn.to/16rfy0B。有关 Unity 容器中的拦截的更多信息,请参阅 MSDN 库文章“使用 Unity 进行拦截” bit.ly/1cWCnwM

拦截基于任务的异步模式 (TAP) 异步方法

此拦截机制足够简单,但如果被拦截方法表示一个返回 Task 对象的异步操作会怎样?在某种程度上,实际上什么也没有改变:方法被调用并返回一个值(Task 对象),或者引发一个异常,因此可像其他方法一样拦截此方法。但您可能对处理异步操作的实际结果感兴趣,而不是表示它的 Task。例如,您可能想要记录 Task 的返回值,或者处理 Task 可能产生的任何异常。

幸运的是,获得表示此操作结果的实际对象可使此异步模式的拦截变得相对简单。其他异步模式更难以拦截:在异步编程模型 (bit.ly/ICl8aH) 中,两个方法表示一个异步操作,而在基于事件的异步模式 (bit.ly/19VdUWu) 中,异步操作由一个启动此操作的方法和一个发出信号通知其完成的相关事件表示。

为实现异步 TAP 操作的拦截,您可以将此方法返回的 Task 替换成在原始 Task 完成后执行必要后期处理的新 Task。被拦截方法的调用程序将收到与此方法的签名相匹配的新 Task,并且将观察被拦截方法的实现结果,以及此拦截行为执行的任何额外处理。

我们将开发基本 TAP 异步操作拦截方法的实现示例,在该方法中,我们想要记录异步操作的完成。您可以改写此示例,以便创建您自己的能够拦截异步操作的行为。

简单示例

我们举一个简单的示例:拦截返回非泛型 Task 的异步方法。我们需要能够检测到被拦截方法返回一个 Task,然后将此 Task 替换成执行相应日志记录的新 Task。

我们可将图 2 中所示的“no op”拦截行为作为起点。

图 2 简单拦截

 

public class LoggingAsynchronousOperationInterceptionBehavior 
  : IInterceptionBehavior
{
  public IMethodReturn Invoke(IMethodInvocation input,
    GetNextInterceptionBehaviorDelegate getNext)
  {
    // Execute the rest of the pipeline and get the return value
    IMethodReturn value = getNext()(input, getNext);
    return value;
  }
  #region additional interception behavior methods
  public IEnumerable<Type> GetRequiredInterfaces()
  {
    return Type.EmptyTypes;
  }
  public bool WillExecute
  {
    get { return true; }
  }
  #endregion
}

下一步,我们添加一些代码,用于检测任务返回方法并将返回的 Task 替换成记录此结果的新包装 Task。为实现这一点,调用输入对象上的 CreateMethodReturn 来创建一个新的 IMethodReturn 对象,并且此对象表示此行为中的新 CreateWrapperTask 方法创建的包装 Task,如图 3 所示。

图 3 返回 Task

public IMethodReturn Invoke(IMethodInvocation input,
  GetNextInterceptionBehaviorDelegate getNext)
{
  // Execute the rest of the pipeline and get the return value
  IMethodReturn value = getNext()(input, getNext);
  // Deal with tasks, if needed
  var method = input.MethodBase as MethodInfo;
  if (value.ReturnValue != null
    && method != null
    && typeof(Task) == method.ReturnType)
  {
    // If this method returns a Task, override the original return value
    var task = (Task)value.ReturnValue;
    return input.CreateMethodReturn(this.CreateWrapperTask(task, input),
      value.Outputs);
  }
  return value;
}

这个新 CreateWrapperTask 方法返回一个等待原始 Task 完成并记录其结果的 Task,如图 4 所示。如果此任务引发了一个异常,则此方法将在记录它后再次引发异常。注意,这种实现没有改变原始 Task 的结果,但不同行为可能替代或忽视原始 Task 可能引发的异常。

图 4 记录结果

private async Task CreateWrapperTask(Task task,
  IMethodInvocation input)
{
  try
  {
    await task.ConfigureAwait(false);
    Trace.TraceInformation("Successfully finished async operation {0}",
      input.MethodBase.Name);
  }
  catch (Exception e)
  {
    Trace.TraceWarning("Async operation {0} threw: {1}",
      input.MethodBase.Name, e);
    throw;
  }
}

处理泛型

处理返回 Task<T> 的方法更加复杂,尤其是如果您想要避免对性能产生影响。现在我们将何谓“T”的问题放在一边,先假设我们已经知道。如图 5 所示,我们能够为已知的“T”编写可处理 Task<T> 的泛型方法,从而利用 C# 5.0 中提供的异步语言功能。

图 5 处理 Task<T> 的泛型方法

private async Task<T> CreateGenericWrapperTask<T>(Task<T> task,
  IMethodInvocation input)
{
  try
  {
    T value = await task.ConfigureAwait(false);
    Trace.TraceInformation("Successfully finished async operation {0} with value: {1}",
      input.MethodBase.Name, value);
    return value;
  }
  catch (Exception e)
  {
    Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
    throw;
  }
}

与这个简单示例一样,此方法仅进行记录,而不改变原始行为。但由于包装的 Task 现在返回了一个值,因此在需要时此行为还可替换该值。

 我们如何调用此方法来获得替代 Task?我们需要借助反射,从而从被拦截方法的泛型返回类型中提取 T,为该 T 创建此泛型方法的封闭版本,然后从中创建一个委托,并最终调用该委托。这个过程的成本可能非常高,因此最好缓存这些委托。如果该 T 是此方法签名的一部分,在不知道该 T 的情况下我们将无法创建方法的委托并调用它,因此,我们将先前的方法拆分成两个方法:一个具有所需的签名,一个可从 C# 语言功能中获益,如图 6 所示。

图 6 拆分委托创建方法

private Task CreateGenericWrapperTask<T>(Task task, IMethodInvocation input)
{
  return this.DoCreateGenericWrapperTask<T>((Task<T>)task, input);
}
private async Task<T> DoCreateGenericWrapperTask<T>(Task<T> task,
  IMethodInvocation input)
{
  try
  {
    T value = await task.ConfigureAwait(false);
    Trace.TraceInformation("Successfully finished async operation {0} with value: {1}",
      input.MethodBase.Name, value);
    return value;
  }
  catch (Exception e)
  {
    Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
    throw;
  }
}

下一步,我们更改拦截方法,以便使用正确的委托包装原始任务,这是我们通过调用新 GetWrapperCreator 方法并传递预计任务类型获得的任务。我们不需要非泛型 Task 的特例,因为它像泛型 Task<T> 一样能够适应此委托方法。图 7 显示了更新的 Invoke 方法。

图 7 更新的 Invoke 方法

public IMethodReturn Invoke(IMethodInvocation input,
  GetNextInterceptionBehaviorDelegate getNext)
{
  IMethodReturn value = getNext()(input, getNext);
  var method = input.MethodBase as MethodInfo;
  if (value.ReturnValue != null
    && method != null
    && typeof(Task).IsAssignableFrom(method.ReturnType))
  {
    // If this method returns a Task, override the original return value
    var task = (Task)value.ReturnValue;
    return input.CreateMethodReturn(
      this.GetWrapperCreator(method.ReturnType)(task, input), value.Outputs);
  }
  return value;
}

剩下的是实现 GetWrapperCreator 方法。此方法将执行成本高昂的反射调用,以便创建这些委托并使用 ConcurrentDictionary 来缓存它们。这些包装创建程序委托为 Func<Task, IMethodInvocation, Task> 类型;我们想获得原始任务以及表示调用(对调用异步方法的操作进行调用)的 IMethodInvocation 对象,并且返回包装 Task。如图 8 所示。

图 8 实现 GetWrapperCreator 方法

private readonly ConcurrentDictionary<Type, Func<Task, IMethodInvocation, Task>>
  wrapperCreators = new ConcurrentDictionary<Type, Func<Task,
  IMethodInvocation, Task>>();
private Func<Task, IMethodInvocation, Task> GetWrapperCreator(Type taskType)
{
  return this.wrapperCreators.GetOrAdd(
    taskType,
    (Type t) =>
    {
      if (t == typeof(Task))
      {
        return this.CreateWrapperTask;
      }
      else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>))
      {
        return (Func<Task, IMethodInvocation, Task>)this.GetType()
          .GetMethod("CreateGenericWrapperTask",
             BindingFlags.Instance | BindingFlags.NonPublic)
          .MakeGenericMethod(new Type[] { t.GenericTypeArguments[0] })
          .CreateDelegate(typeof(Func<Task, IMethodInvocation, Task>), this);
      }
      else
      {
        // Other cases are not supported
        return (task, _) => task;
      }
    });
}

对于非泛型 Task 的情况,无需反射,并且可按原样将现有非泛型方法作为所需委托。在处理 Task<T> 时会执行必要的反射调用,以便创建相应委托。最后,我们无法支持其他任何 Task 类型,因为我们不知道如何创建它,因此返回刚刚返回原始任务的 no-op 委托。

此行为现在可在被拦截的对象上使用,并且对于返回了值以及引发了异常的情况,此行为将记录被拦截对象的方法所返回的任务结果。图 9 中的示例显示了如何配置容器以拦截对象并使用这种新行为,并且显示了在调用不同方法时所获得的输出。

图 9 配置容器以拦截对象并使用新行为

using (var container = new UnityContainer())
{
  container.AddNewExtension<Interception>();
  container.RegisterType<ITestObject, TestObject>(
    new Interceptor<InterfaceInterceptor>(),
    new InterceptionBehavior<LoggingAsynchronousOperationInterceptionBehavior>());
  var instance = container.Resolve<ITestObject>();
  await instance.DoStuffAsync("test");
  // Do some other work
}
Output:
vstest.executionengine.x86.exe Information: 0 : ­
  Successfully finished async operation ­DoStuffAsync with value: test
vstest.executionengine.x86.exe Warning: 0 : ­
  Async operation DoStuffAsync threw: ­
    System.InvalidOperationException: invalid
   at AsyncInterception.Tests.AsyncBehaviorTests2.TestObject.<­
     DoStuffAsync>d__38.MoveNext() in d:\dev\interceptiontask\­
       AsyncInterception\­AsyncInterception.Tests\­
         AsyncBehaviorTests2.cs:line 501
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(­Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.­
     HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at AsyncInterception.LoggingAsynchronousOperationInterceptionBehavior.<­
     CreateWrapperTask>d__3.MoveNext() in d:\dev\interceptiontask\­
       AsyncInterception\AsyncInterception\­
         LoggingAsynchronousOperationInterceptionBehavior.cs:line 63

实现跟踪

图 9 所得输出所示,此实现中使用的方法略微改变了异常的堆栈跟踪,从而反映了在等待此任务时再次引发该异常的方式。一种替代方法可使用 ContinueWith 方法和 TaskCompletionSource<T>(而不是 await 关键字)来避免此问题,代价是实现变得更加复杂(并且可能成本更高),如图 10 所示。

图 10 使用 ContinueWith 而不是 await 关键字

private Task CreateWrapperTask(Task task, IMethodInvocation input)
{
  var tcs = new TaskCompletionSource<bool>();
  task.ContinueWith(
    t =>
    {
      if (t.IsFaulted)
      {
        var e = t.Exception.InnerException;
        Trace.TraceWarning("Async operation {0} threw: {1}",
          input.MethodBase.Name, e);
        tcs.SetException(e);
      }
      else if (t.IsCanceled)
      {
        tcs.SetCanceled();
      }
      else
      {
        Trace.TraceInformation("Successfully finished async operation {0}",
          input.MethodBase.Name);
        tcs.SetResult(true);
      }
    },
    TaskContinuationOptions.ExecuteSynchronously);
  return tcs.Task;
}

总结

我们讨论了拦截异步方法的多种策略,并在记录异步操作完成的示例中演示了这些策略。您可以改写此示例,以便创建您自己的支持异步操作的拦截行为。可以从 msdn.microsoft.com/magazine/msdnmag0214 获取此示例的完整源代码。

Fernando Simonazzi 是一位软件开发人员和架构师,有超过 15 年的专业经验。他参与过 Microsoft 模式和实施方案项目,包括多个版本的 Enterprise Library、Unity、CQRS Journey 和 Prism。Simonazzi 还是 Clarius Consulting 的准会员。

Grigori Melnik 博士 是 Microsoft 模式和实施方案组的首席项目经理。最近,他在开展 Microsoft Enterprise Library、Unity、CQRS Journey 和 NUI 模式项目。在此之前,他做了很长时间的研究员和软件工程师,他还记得用 Fortran 编程的乐趣。Melnik 博士的博客地址为 blogs.msdn.com/agile

衷心感谢以下技术专家对本文的审阅:Stephen Toub (Microsoft)