【问题标题】:Globally catch exceptions thrown from WCF async calls in a background thread在后台线程中全局捕获 WCF 异步调用引发的异常
【发布时间】:2013-10-10 02:18:03
【问题描述】:

我有一个与 WCF 服务通信的 WPF 应用程序。我目前正在使用以下基于 async 的模式从我的 ViewModel(我正在使用 MVVM 模式)调用我的 WCF 服务:

public async override void MyCommandImplementation()
{
    using (var proxy = new MyProxy())
    {
        var something = await proxy.GetSomethingAsync();
        this.MyProperty = something;
    }
}

由于我遵循 MVVM 模式,我的 ViewModel 公开了 ICommand 公共属性,因此相关的命令实现不会返回 Task<T> 对象,因为它们就像事件处理程序一样。所以异常处理实际上非常简单,即我可以使用以下模式捕获从我的 WCF 服务抛出的任何异常:

public async override void MyCommandImplementation()
{
    try
    {
        using (var proxy = new MyProxy())
        {
            var something = await proxy.GetSomethingAsync();
        }
    }
    catch (FaultException<MyFaultDetail> ex)
    {
        // Do something here
    }
}

到目前为止一切顺利,如果服务器抛出异常,由于自定义 WCF 行为,该异常会自动转换为 SOAP 错误。

由于我有一些常见的异常可以在我的服务中随处抛出(例如,每个 WCF 操作都可以抛出一个 AuthenticationException ,这将在客户端转换为 FaultException&lt;AuthenticationFaultDetail&gt; 异常),我决定在我的应用程序的一个常见位置处理一些异常,即通过处理Application.DispatcherUnhandledException 事件。这工作得很好,我可以在任何地方捕获我所有的FaultException&lt;AuthenticationFaultDetail&gt; 异常,向用户显示错误消息,并阻止应用程序退出:

private static void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
    // Exception handler that handles some common exceptions
    // such as FaultException<AuthenticationFaultDetail>
    if (GlobalExceptionHandler.HandleException(e.Exception))
    {
        // Expected exception, so we're fine
        e.Handled = true;
    }
    else
    {
        // We're not fine. We couldn't handle the exception
        // so we'll exit the application
        // Log...etc.
    }
}

由于async 模式和await 关键字之前/之后的同步上下文切换,FaultException 在 UI 线程中被抛出,所以一切运行良好。

我的问题是,在我的 UI 线程之外的另一个线程中可能会引发其他异常,例如在 await proxy.GetSomethingAsync(); 行抛出 EndPointNotFoundException 的情况下(服务器端 WCF 服务关闭的情况)。

这些异常不会在Application.DispatcherUnhandledException 事件处理程序中处理,因为它们不会在 UI 线程中引发。我可以在AppDomain.UnhandledException 事件中处理它们,但除了做一些日志记录并退出应用程序之外我什么也做不了(基本上没有类似“e.Handled”的属性)。

所以我的问题是:如果在我的应用程序的一个地方进行异步 WCF 调用,我该如何处理后台线程中抛出的异常?

我现在能想到的最好的方法如下:

public class ExceptionHandler : IDisposable
{
    public void HandleException(Exception ex)
    {
        // Do something clever here
    }
    public void Dispose()
    {
        // Do nothing here, I just want the 'using' syntactic sugar
    }
}

...

public async override void MyCommandImplementation()
{
    using (var handler = new ExceptionHandler())
    {
        try
        {
            using (var proxy = new MyProxy())
            {
                var something = await proxy.GetSomethingAsync();
            }
        }
        catch (FaultException<MyFaultDetail> ex)
        {
            // Do something here
        }
        catch (Exception ex)
        {
            // For other exceptions in any thread
            handler.HandleException(ex);
        }
    }
}

但这需要我重构大量代码(每次我异步调用 Web 服务时)。

任何能让我重构大量代码的想法都会有所帮助。

【问题讨论】:

    标签: c# wpf multithreading exception-handling async-await


    【解决方案1】:

    通常,我不是集中/全局异常处理的忠实粉丝。我个人的偏好是要么处处处理异常,要么编写自己的代理包装对象来处理/翻译预期的错误异常。

    也就是说,您可以考虑一种方法(尽管它需要修改所有命令)。

    首先,将实际逻辑分解为async Task 方法,如下所示:

    public async Task MyCommandAsync()
    {
      try
      {
        using (var proxy = new MyProxy())
        {
          var something = await proxy.GetSomethingAsync();
        }
      }
      catch (FaultException<MyFaultDetail> ex)
      {
        // Do something here
      }
    }
    
    public async override void MyCommandImplementation()
    {
      MyCommandAsync();
    }
    

    通常,我建议使用async Task ExecuteAsync 方法实现async ICommands 并匹配async void Execute,这样就可以做到await ExecuteAsync();。我上面的示例几乎相同,除了 async void 方法是 not awaiting Task。这很危险,我将在下面解释。

    将您的逻辑保存在 async Task 中会给您一个巨大的优势:您可以更轻松地进行单元测试。此外,async Task 方法具有不同的异常处理,您可以(ab)使用它们来解决您的问题。

    async Task 方法 - 如果返回的 Task 永远不会是 awaited - 将引发 TaskScheduler.UnobservedTaskException。请注意,这不会使您的进程崩溃(从 .NET 4.5 开始);您的处理程序必须决定最佳响应。由于您的async void 方法不是 awaiting 是您的async Task 方法返回的Task,因此任何异常都将在UnobservedTaskException 中结束。

    这样会起作用,但它有一个严重的副作用:任何未观察到的 Task 异常将最终出现在同一个处理程序中(不仅仅是来自您的 ICommands 的处理程序)。在 .NET 4.5 中将未观察到的任务异常更改为默认忽略的原因是因为 async 代码中的这种情况不再不寻常。例如,考虑以下代码,它将尝试从两个不同的 url 下载并获取第一个响应:

    async Task<string> GetMyStringAsync()
    {
      var task1 = httpClient.GetAsync(url1);
      var task2 = httpClient.GetAsync(url2);
      var completedTask = await Task.WhenAny(task1, task2);
      return await completedTask;
    }
    

    在这种情况下,如果较慢的 url 导致错误,那么该异常将被发送到UnobservedTaskException

    【讨论】:

    • 1/2 +1,非常感谢这个答案,这非常有帮助并且解释得很好。我绝对同意“async Task MyCommandAsync”/“async override void MyCommandImplementation”模式,因为正如您所说,它使代码更具可测试性。 UnobservedTaskException 事件很有趣,但在 IMO 中有点危险。它的行为在 .Net4 和 .Net4.5 之间发生了变化,并且只有在 GC 完成其工作时才会引发该事件。
    • 2/2 所以我认为的答案是将ICommand 实现和异步处理分成两种方法。一个进行异步处理并返回一个任务。第二个等待第一个,因为我需要一种全局异常处理,我可以编写一次逻辑并在第二种方法中使用await MyCommandAsync().ContinueWith(z =&gt; MyGlobalExceptionHandler(z.Exception)...); 调用它。
    • 哦,顺便说一句,我刚刚发现你是 MSDN 上 excellent article 的作者,非常感谢 :)
    【解决方案2】:

    我目前正在研究面向方面的编程。所以我使用Postsharps 方法拦截方面进行服务调用。它允许您集中用于调用服务的代码等。我还将它用于日志记录和线程同步。

    编辑:我刚刚发现 await 关键字尚不支持。 (即将在 3.1 中)。

    这是一个例子:

    [Serializable]
    internal class ServiceCall : MethodInterceptionAspect
    {
      public override void OnInvoke(MethodInterceptionArgs args)
      {
        try
        {
            args.Proceed();
        }
        catch (FaultException<DCErrorMessage> f)
        {
            showError(f.Detail.Message + "\r\n" + f.Detail.Callstack, f.Detail.Title);
        }
        catch (Exception e)
        {
            showError(e.Message, "Error");
        }
    }
    

    这是它的使用方法

    [ServiceCall]
    public Something getSomethingAsync()
    {
       return await _client.getSomethingAsync();
    }
    

    方面也可以应用于整个类或程序集。

    【讨论】:

    • 感谢您的回答。如果支持async,这将是解决问题的好方法。另一个问题是不幸的是我们目前不使用 postsharp,所以这对我们的解决方案来说将是一个巨大的变化。无论如何都要 +1
    猜你喜欢
    • 2011-08-12
    • 2016-03-19
    • 1970-01-01
    • 1970-01-01
    • 2015-01-22
    • 1970-01-01
    • 2012-01-01
    • 1970-01-01
    • 2012-06-05
    相关资源
    最近更新 更多