本文章是由機器翻譯。

非同步程式設計

使用 Unity 攔截功能攔截非同步方法

Fernando Simonazzi

下載代碼示例

統一 (不要混淆與 Unity3D 遊戲引擎) 是一個普通用途、 可擴展的依賴注入容器與攔截支援在任何類型的微軟基於.NET 框架的應用程式中使用。統一設計,並由微軟模式維護 & 實踐團隊 (microsoft.com/practices)。它可以輕鬆地添加到您的應用程式通過 NuGet,和你會發現的學習資源與相關的統一主要樞紐 msdn.com/unity

這篇文章的重點統一截取。截取是一種技術,當您想要修改單個物件的行為而不會影響從同一類的其他物件的行為非常一樣使用修飾器模式時,你會做時很有用 (維琪百科定義 Decorator 模式的可以在這裡找到:bit.ly/1gZZUQu)。截取為在運行時向物件添加新的行為提供了靈活的方法。這些行為通常解決一些橫切關注點,例如日誌記錄或資料驗證。截取常用的作為面向方面程式設計 (AOP) 的基本機制。在統一的運行時攔截功能允許您有效地攔截到的物件的方法調用和執行預處理和後處理這些調用。

截取的統一容器中有兩個主要元件:攔截器和攔截行為。攔截器確定用於攔截到的被攔截的物件中的方法調用,同時攔截行為確定截取的方法調用執行的操作的機制。被攔截的物件提供一個管道攔截行為。時,截獲的方法調用,每個管道中的行為允許檢查,甚至可以修改參數的方法調用,並最終調用原始的方法實施。返回時,每個行為可以檢查或更換的傳回值或由原始的實施或管道中的先前行為引發的異常。最後,原始調用方獲取結果的傳回值,如果有或產生的異常。圖 1 描述了攔截機制。


圖 1 統一攔截機制

有兩種類型的攔截技術:實例截取和類型截取。與實例攔截、 統一動態地創建一個代理物件,在用戶端和目標物件之間插入。代理物件負責然後傳遞到目標物件通過行為作出的用戶端調用的。你可以使用統一實例攔截攔截創建由統一容器和容器之外的物件和你可以使用它來攔截虛擬和非虛擬方法。然而,您不能轉換到目標物件的類型的動態創建的代理類型。與類型攔截、 統一動態地創建新的類型派生從目標物件的類型,並包含處理橫切問題的行為。統一容器在運行時具現化的派生類型的物件。實例截取只能攔截公共實例方法。類型截取可以截獲公共和受保護的虛方法。請記住,由於平臺的限制,統一截取不支援 Windows Phone 和 Windows 應用商店的應用程式開發,雖然核心團結容器不會。

論統一的底漆,請參閱"與統一的依賴關係注入"(微軟模式 & 做法、 2013年) 在 amzn.to/16rfy0B。有關統一容器中截取的詳細資訊,請參閱 MSDN 庫文章,"截獲使用統一,"在 bit.ly/1cWCnwM

攔截基於任務的非同步模式 (TAP) 非同步方法

攔截機制相當簡單,但如果被攔截的方法表示返回的 Task 物件的非同步作業會發生什麼呢?在某種程度上,沒有什麼真正的改動:一種方法調用和返回一個值 (任務的物件) 或引發異常,因此它可以截獲任何其他方法一樣。但你可能感興趣的非同步作業,而不是代表它的任務的實際結果處理。例如,你可能會想要登錄任務的傳回值,或處理任務可能產生的任何異常。

幸運的是,有一個實際的物件,表示操作的結果,使攔截此非同步模式相對簡單。其他非同步模式是一個較為難攔截:在非同步程式設計模型中 (bit.ly/ICl8aH) 的兩種方法表示一個非同步作業,而在事件架構非同步模式 (bit.ly/19VdUWu) 非同步作業由一種方法啟動該操作和關聯的事件,它的完成的信號。

為了實現目標截獲的水龍頭的非同步作業,您可以替換方法與新的任務,執行必要的後處理原始任務完成後返回的任務。被攔截的方法的調用方將收到新任務中匹配的方法的簽名,並將觀察的截取的方法執行,再加上任何額外處理的攔截行為執行結果。

我們會發展到攔截水龍頭非同步作業中,我們想要登錄完成的非同步作業的基本方法的示例實現。您可以修改此示例來創建您自己可以截取的非同步作業的行為。

簡單的案件

讓我們從簡單的情況下開始:攔截返回一個非泛型任務的非同步方法。我們需要能夠檢測到被攔截的方法返回一個任務並替換該任務新執行適當的日誌記錄。

我們可以採取中所示的"沒有 op"攔截行為圖 2 作為起始點。

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

下一步,我們添加代碼,以檢測任務返回方法並返回的任務替換新包裝記錄結果的任務。 若要實現此目的上輸入物件, CreateMethodReturn 調用來創建一個新的 IMethodReturn 物件,表示由在行為中,新的 CreateWrapperTask 方法創建的任務的包裝,如中所示圖 3

圖 3 將任務返回

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 方法返回任務要完成的原始任務等待並記錄其結果,如中所示圖 4。 該方法如果任務導致異常,將再次引發它,記錄它之後。 請注意此實現不會更改原始任務的結果,但不同的行為可以替換或忽略了原有的任務可能會引入的例外情況。

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

泛型處理

處理返回任務 < T > 的方法 是更複雜一些,特別是如果你想要避免服用性能命中。 讓我們現在忽略搞什麼"T"是和假設它已知的問題。 作為圖 5 所示,我們可以編寫一個泛型方法,可以處理任務 < T > 對於已知"T,"利用 C# 5.0 中可用的非同步語言功能。

圖 5 泛型方法來處理任務 < 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;
  }
}

如用最簡單的情況下,該方法只是日誌而不改變原來的行為。 但包裝的任務現在返回一個值,因為該行為還可以替代此值,如果需要。

我們如何可以調用來調用此方法,以獲取用於更換的任務? 我們需要訴諸反射,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 方法並傳遞的預期的任務類型。 我們不需要一種特殊情況為非泛型的任務,因為它可以符合通用任務 < T > 一樣的委託方法。 圖 7 顯示更新後的 Invoke 方法。

圖 7 更新的調用方法

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 < 任務、 IMethodInvocation、 任務 > ; 我們想要獲取原始任務和表示對非同步方法調用的調用的 IMethodInvocation 物件,並返回一個包裝任務。 這顯示在圖 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;
      }
    });
}

為非泛型的任務情況下,沒有反射需要和現有的非泛型方法可以用作需委託如是。 當處理任務 < T >,執行必要的反射調用來創建相應的委託。 最後,我們不能支援任何其他類型,任務類型,如我們不會知道如何創建它,因此一個 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 > 而要避免此問題,而有更複雜 (和可能更昂貴) 的執行,如所示的等待關鍵字圖 10

圖 10 使用 ContinueWith 而不是等待關鍵字

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 年的專業經驗與建築師。他已經貢獻者微軟模式 & 實踐專案,包括企業圖書館、 統一,CQRS 的旅程和棱鏡的幾個版本。Simonazzi 也是副研究員 Clarius 諮詢。

**博士。**Grigori Melnik是一個主要程式管理器上的 Microsoft 模式 & 實踐團隊。這些天他開車的微軟企業庫、 團結、 CQRS 的旅程和 NUI 模式專案。在此之前,他是一個足夠長的時間前的研究員和軟體工程師還記得在 Fortran 程式設計的樂趣。 博士。 在 Melnik 博客 blogs.msdn.com/agile

由於以下的技術專家對本文的審閱:StephenToub (Microsoft)