65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF - 异常处理、FaultException 和 FaultContracts

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (52投票s)

2016年1月29日

CPOL

6分钟阅读

viewsIcon

307855

downloadIcon

2015

WCF - 异常处理、全局异常处理、FaultExceptions 和 FaultContracts

目录

引言

每当我们编写程序时,都应该预料到意外情况,并添加适当的代码来处理这些异常。当我们编写 Web 或 Windows 应用程序时,我们在方法中使用 try/catch 块来捕获异常,并且我们还在 UI 中显示用户友好的消息。

但是,当我们使用 WCF 服务时,情况并非如此直接,因此

  • 如何在 WCF 服务中处理异常?
  • 如何将用户友好的错误消息传播给消费 WCF 服务的客户端?
  • 如何将业务规则验证消息发送到 WCF 客户端?

WCF 消费者可以使用任何技术,因此发送 CLR 异常没有意义,为了避免这种情况,我们需要在 WCF 服务中进行更好的异常处理。

为了帮助 WCF 服务,.NET 提供了 SoapFaults、FaultException<T>,其中 T 可以是任何可序列化的类型。使用 SoapFaults,我们可以避免将 CLR 异常对象发送到客户端。

在本文中,我们将通过使用一个示例 WCF 服务实现来学习以上所有提到的要点。

创建演示项目(s)

为了演示异常处理,我创建了一个简单的 WCF 服务应用程序(名为“DemoService”),它有一个 OperationContractDivide”。

WCF Sample Project for Demonstrating Exception Handling

然后,我重构了所有服务并为默认文件命名为 DemoService

下面是修改后的 IDemoService.cs 文件内容。

namespace DemoService
{    
    [ServiceContract]
    public interface IDemoService
    {
        [OperationContract]
        int Divide(int n1, int n2);
    }

    //TODO: Add data contract Fault Exception
}

下面是修改后的 DemoService.svc.cs 文件内容。

namespace DemoService
{    
    // NOTE: In order to launch WCF Test Client for testing this service, 
    // please select Service1.svc or Service1.svc.cs at the Solution Explorer 
    // and start debugging.
    public class DemoService : IDemoService
    {
        public int Divide(int n1, int n2)
        {
            return n1 / n2;
        }
    }
}

为了演示示例,我将创建一个控制台应用程序(名为“DemoClient”),它将消费 DemoService

WCF Client Sample

为了消费 WCF 服务,我将使用 **AddServiceReference** 菜单(右键单击 _References_ 文件夹)将 **Service Reference** 添加到 DemoClient 控制台应用程序。

Add WCF Service Reference to a Client Application

现在,我们的 DemoClient 控制台应用程序已添加了 ServiceReferenceProgram.cs 已修改如下:

namespace DemoClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Divide***\n\n");

            Console.Write("Enter Number 1: ");
            string line1 = Console.ReadLine(); // Read string from console

            Console.Write("Enter Number 2: ");
            string line2 = Console.ReadLine(); // Read string from console

            int num1;
            int num2;

            if (int.TryParse(line1, out num1) && 
                int.TryParse(line2, out num2)) // Try to parse the string as an integer
            {
                DemoServiceReference.DemoServiceClient client = 
                                     new DemoServiceReference.DemoServiceClient(); ;

                try
                {
                    client.Open();

                    Console.WriteLine("\nOutput: {0}", 
                    client.Divide(num1, num2)); // divide the numbers and display the result.

                    client.Close();

                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                    client.Abort();
                }
            }
            else
            {
                Console.WriteLine("Invalid inputs!.");
            }

            Console.ReadKey();
        }
    }

上面的代码块将显示控制台并接受两个数字作为输入。这两个数字将传递给 DemoService Divide 方法,并将结果显示在控制台中。

让我们将此视为演示的基础代码,现在 **Solution Explorer** 将具有以下结构:

Sample Solution Explorer WCF

.NET 异常对象

好的。现在让我们运行我们的应用程序。

为了启动 WCF 测试客户端来测试此服务,请将 DemoService 设置为启动项目,并在 **Solution Explorer** 中选择 _Service1.svc_ 或 _Service1.svc.cs_,然后开始调试。使用 WCF 测试客户端,我得到了如下示例输入的输出:

WCF Test Client

由于我们已经创建了一个控制台应用程序来消费我们的演示服务,我已经将 DemoClient 项目设置为启动项目。

Set as Startup Project

运行应用程序后,我输入的值为零 (0)。

WCF Exception Handling

众所周知,除以零会引发异常,这里也发生了同样的情况,但我们得到的是什么异常?

"The server was unable to process the request due to an internal error. 
For more information about the error, either turn on IncludeExceptionDetailInFaults 
(either from ServiceBehaviorAttribute or from the <serviceDebug> configuration behavior) 
on the server in order to send the exception information back to the client, 
or turn on tracing as per the Microsoft .NET Framework SDK documentation 
and inspect the server trace logs."

WCF FaultException Caught

从上面的错误消息来看,我们无法获得有关实际错误的线索。这个通用的错误消息会让客户端对服务引发的错误感到困惑。

正如通用错误消息所示,我们可以通过设置一个配置键来启用错误中的异常详细信息。

我已将 DemoServiceweb.config 修改为 includeExceptionDetailInFaults="true",以便错误中包含异常详细信息。

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- To avoid disclosing metadata information, 
               set the value below to false before deployment -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, 
               set the value below to true. Set to false before deployment 
               to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
 <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <!--
        To browse web app root directory during debugging, set the value below to true.
        Set to false before deployment to avoid disclosing web app folder information.
      -->
    <directoryBrowse enabled="true"/>
  </system.webServer>

</configuration>

在启用 includeExceptionDetailInFaults="true" 之前,我们得到了以下异常详细信息:

WCF Exception Handling

但在启用之后,我们在异常详细信息中获得了更多信息,如下所示:

WCF Exception Handling

异常是 CLR 的概念,它在 CLR 之外暴露没有意义,而且我们在开发环境(调试目的)之外也不应该这样做,因为此异常包含潜在的危险信息,例如服务堆栈跟踪。

我们应该使用强类型 FaultException 来将异常发送到消费我们 WCF 服务的客户端。

FaultExceptions

正如我们在上一节中看到的,每当 WCF 遇到未处理的异常(非类型化错误)时,它都会抛出 FaultException

我已更新 DemoService.svc.cs 以在 DivideByZeroException 发生时抛出非类型化的 FaultException

public int Divide(int n1, int n2)
{
    try
    {
        return n1 / n2;
    }
    catch (DivideByZeroException e)
    {
        throw new FaultException(e.Message);
    }
}

我还在 DemoClientprogram.cs 中更新了以如下方式捕获 FaultException

static void Main(string[] args)
{
    Console.WriteLine("***Divide***\n\n");

    Console.Write("Enter Number 1: ");
    string line1 = Console.ReadLine(); // Read string from console

    Console.Write("Enter Number 2: ");
    string line2 = Console.ReadLine(); // Read string from console

    int num1;
    int num2;

    if (int.TryParse(line1, out num1) && 
        int.TryParse(line2, out num2)) // Try to parse the string as an integer
    {
        DemoServiceReference.DemoServiceClient client = 
                             new DemoServiceReference.DemoServiceClient(); ;

        try
        {
            client.Open();

            Console.WriteLine("\nOutput: {0}", 
               client.Divide(num1, num2)); // divide the numbers and display the result.

            client.Close();
        }
        catch (FaultException e)
        {
            Console.WriteLine(e.Message);
            client.Abort();
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            client.Abort();
        }
    }
    else
    {
        Console.WriteLine("Invalid inputs!.");
    }

    Console.ReadKey();
}

当用户再次使用零输入调用客户端应用程序时,我们将通过 FaultException 获得正确的异常。

FaultException<TDetail>

在上一节中,我们看到了如何使用非类型化 FaultException 将异常详细信息传播到客户端。在本节中,我们将学习如何使用强类型 FaultException 将异常详细信息传播到 WCF 消费者。

这有助于发送类型化的错误数据,其中 TDetail 将携带错误信息。这里,OperationContract Divide[FaultContract(typeof(DivideByZeroFault))] 装饰,以告知最终客户端此操作合同可能会返回 DivideByZeroFault 错误。错误可以是任何可序列化的类型。在这种情况下,DivideByZeroFault 是一个数据合同,如下所示(在 IDemoService.cs 中)。

[DataContract]
public class DivideByZeroFault
{
    [DataMember]
    public bool Result { get; set; }
    [DataMember]
    public string Message { get; set; }
    [DataMember]
    public string Description { get; set; }
}

并且我用 FaultContractAttribute 装饰了 Divide 操作合同。

[ServiceContract]
public interface IDemoService
{
    [OperationContract]
    [FaultContract(typeof(DivideByZeroFault))]
    int Divide(int n1, int n2);
}

同样,我修改了 DemoService.svc.cs 以发送强类型 FaultException

public int Divide(int n1, int n2)
{
    try
    {
        return n1 / n2;
    }
    catch (DivideByZeroException e)
    {
        DivideByZeroFault fault = new DivideByZeroFault();

        fault.Result = false;
        fault.Message = e.Message;
        fault.Description = "Cannot divide by zero.";

        throw new FaultException<DivideByZeroFault>(fault);     
    }
}

完成服务端的更改后,请务必更新服务引用。

Update WCF Service Reference

现在运行我们的 DemoClient 并输入零以测试 FaultContract

Strongly Typed Fault Exception WCF

我们收到上述消息是因为我们还没有添加捕获 FaultException<DemoServiceReference.DivideByZeroFault> 的代码。为了处理这个问题,我在 Program.cs 文件的 try-catch 中添加了以下代码。

catch (FaultException<DemoServiceReference.DivideByZeroFault> e)
{
    Console.WriteLine("Message: {0}, Description: {1}", 
                       e.Detail.Message, e.Detail.Description);
    client.Abort();
}

现在,如果使用零作为输入运行应用程序,您将获得如下所示的强类型 faultexception

现在,我为验证目的添加了另一个 DataContract,如下所示:

[DataContract]
public class ValidationFault
{
    [DataMember]
    public bool Result { get; set; }
    [DataMember]
    public string Message { get; set; }
    [DataMember]
    public string Description { get; set; }
}

并将 OperationContract Divide 用上述 ValidationFault FaultContract 进行了装饰。

[OperationContract]
[FaultContract(typeof(DivideByZeroFault))]
[FaultContract(typeof(ValidationFault))]
int Divide(int n1, int n2);

现在 DemoService.svc.cs 中的 Divide 方法实现如下所示:

public int Divide(int n1, int n2)
{
    try
    {
        if (n1 == 0 && n2 == 0)
        {
            ValidationFault fault = new ValidationFault();

            fault.Result = false;
            fault.Message = "Numbers cannot be zero";
            fault.Description = "Invalid numbers";

            throw new FaultException<ValidationFault>(fault);
        }

        return n1 / n2;
    }
    catch (DivideByZeroException e)
    {
        DivideByZeroFault fault = new DivideByZeroFault();

        fault.Result = false;
        fault.Message = e.Message;
        fault.Description = "Cannot divide by zero.";

        throw new FaultException<DivideByZeroFault>(fault);
    }
}

为了捕获此验证错误,我在 Program.cstry/catch 部分添加了以下代码块。

catch (FaultException<DemoServiceReference.ValidationFault> e)
{
    Console.WriteLine("Message: {0}, Description: {1}", 
                       e.Detail.Message, e.Detail.Description);
    client.Abort();
}

输出

倒带

FaultException 与 Exception

ExceptionFaultException 不同。如果我们从 WCF 服务抛出普通异常,那么消费该服务的客户端将收到一个通用的错误,该错误不会提供有关问题的任何详细信息。相反,我们应该使用 FaultException,因为它被序列化,可以为客户端提供格式化的异常。

异常

throw new Exception("Divide by zero");

通过抛出上述异常,客户端将收到如下所示的异常:

"The server was unable to process the request due to an internal error. 
For more information about the error, either turn on IncludeExceptionDetailInFaults 
(either from ServiceBehaviorAttribute or from the <serviceDebug> configuration behavior) 
on the server in order to send the exception information back to the client, 
or turn on tracing as per the Microsoft .NET Framework SDK documentation 
and inspect the server trace logs."

FaultException

​throw new FaultException("Divide by zero");

在上面的 FaultException 示例中,客户端将收到如下所示的异常消息:

Divide by zero

始终建议发送强类型 SOAP 错误,而不是托管的异常对象。FaultException 可用于调试目的,或者当服务合同未定义错误合同时使用。

FaultContract 与 FaultException

我们可能会想,既然可以将非类型化的 FaultException 发送到客户端,那么为什么还需要通过向 OpertionContract 添加 FaultContractAttribute 并抛出 FaultException<TDetail> 来使用强类型 FaultException

最好发送客户端期望接收的强类型 SOAP 错误,并确保包含最终客户端应该知道的最小数据。非类型化错误对于调试目的很有用。

注意:单向操作不支持 FaultContractAttribute

代码

供您参考,我已将完整的源代码附加到本文中。为方便参考,我下面展示了其中一些文件。

IDemoService.cs (DemoService)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;

namespace DemoService
{
    [ServiceContract]
    public interface IDemoService
    {
        [OperationContract]
        [FaultContract(typeof(DivideByZeroFault))]
        [FaultContract(typeof(ValidationFault))]
        int Divide(int n1, int n2);
    }

    [DataContract]
    public class DivideByZeroFault
    {
        [DataMember]
        public bool Result { get; set; }
        [DataMember]
        public string Message { get; set; }
        [DataMember]
        public string Description { get; set; }
    }

    [DataContract]
    public class ValidationFault
    {
        [DataMember]
        public bool Result { get; set; }
        [DataMember]
        public string Message { get; set; }
        [DataMember]
        public string Description { get; set; }
    }
}

DemoService.svc.cs (DemoService)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;

namespace DemoService
{
    // NOTE: In order to launch WCF Test Client for testing this service, 
    // please select Service1.svc or Service1.svc.cs at the Solution Explorer 
    // and start debugging.
    public class DemoService : IDemoService
    {
        public int Divide(int n1, int n2)
        {
            try
            {
                if (n1 == 0 && n2 == 0)
                {
                    ValidationFault fault = new ValidationFault();

                    fault.Result = false;
                    fault.Message = "Numbers cannot be zero";
                    fault.Description = "Invalid numbers";

                    throw new FaultException<ValidationFault>(fault);
                }

                return n1 / n2;
            }
            catch (DivideByZeroException e)
            {
                DivideByZeroFault fault = new DivideByZeroFault();

                fault.Result = false;
                fault.Message = e.Message;
                fault.Description = "Cannot divide by zero.";

                throw new FaultException<DivideByZeroFault>(fault);
            }
        }
    }
}

Program.cs (DemoClient)

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;

namespace DemoClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Divide***\n\n");

            Console.Write("Enter Number 1: ");
            string line1 = Console.ReadLine(); // Read string from console

            Console.Write("Enter Number 2: ");
            string line2 = Console.ReadLine(); // Read string from console

            int num1;
            int num2;

            if (int.TryParse(line1, out num1) && 
                int.TryParse(line2, out num2)) // Try to parse the string as an integer
            {
                DemoServiceReference.DemoServiceClient client = 
                                     new DemoServiceReference.DemoServiceClient(); ;

                try
                {
                    client.Open();

                    Console.WriteLine("\nOutput: {0}", 
                    client.Divide(num1, num2)); // divide the numbers and display the result.

                    client.Close();

                }
                catch (TimeoutException e)
                {
                    Console.WriteLine("The service operation timed out. " + e.Message);
                    client.Abort();
                }
                // Catch the contractually specified SOAP fault raised here as an exception. 
                catch (FaultException<DemoServiceReference.ValidationFault> e)
                {
                    Console.WriteLine("Message: {0}, Description: {1}", 
                                       e.Detail.Message, e.Detail.Description);
                    client.Abort();
                }
                // Catch the contractually specified SOAP fault raised here as an exception. 
                catch (FaultException<DemoServiceReference.DivideByZeroFault> e)
                {
                    Console.WriteLine("Message: {0}, Description: {1}", 
                                       e.Detail.Message, e.Detail.Description);
                    client.Abort();
                }
                // Catch unrecognized faults. This handler receives exceptions thrown by WCF 
                // services when ServiceDebugBehavior.IncludeExceptionDetailInFaults  
                // is set to true or when un-typed FaultExceptions raised.
                catch (FaultException e)
                {
                    Console.WriteLine(e.Message);
                    client.Abort();
                }
                // Standard communication fault handler. 
                catch (CommunicationException e)
                {
                    Console.WriteLine("There was a communication problem. " + 
                                       e.Message + e.StackTrace);
                    client.Abort();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                    client.Abort();
                }
            }
            else
            {
                Console.WriteLine("Invalid inputs!.");
            }

            Console.ReadKey();
        }
    }
}

摘要

在本文中,我通过一个示例 WCF 服务和控制台应用程序解释了 WCF - FaultExceptionFaultContract。希望您喜欢这篇文章,并从中获得了知识上的增值。

我已投入时间和精力来撰写我所有的文章,请不要忘记为您改进本文和未来文章的质量标记您的投票、建议和反馈。

历史

  • 2016年1月29日:初始版本
© . All rights reserved.