WCF - 异常处理、FaultException 和 FaultContracts






4.63/5 (52投票s)
WCF - 异常处理、全局异常处理、FaultExceptions 和 FaultContracts
目录
引言
每当我们编写程序时,都应该预料到意外情况,并添加适当的代码来处理这些异常。当我们编写 Web 或 Windows 应用程序时,我们在方法中使用 try
/catch
块来捕获异常,并且我们还在 UI 中显示用户友好的消息。
但是,当我们使用 WCF 服务时,情况并非如此直接,因此
- 如何在 WCF 服务中处理异常?
- 如何将用户友好的错误消息传播给消费 WCF 服务的客户端?
- 如何将业务规则验证消息发送到 WCF 客户端?
WCF 消费者可以使用任何技术,因此发送 CLR 异常没有意义,为了避免这种情况,我们需要在 WCF 服务中进行更好的异常处理。
为了帮助 WCF 服务,.NET 提供了 SoapFault
s、FaultException<T>
,其中 T
可以是任何可序列化的类型。使用 SoapFaults
,我们可以避免将 CLR 异常对象发送到客户端。
在本文中,我们将通过使用一个示例 WCF 服务实现来学习以上所有提到的要点。
创建演示项目(s)
为了演示异常处理,我创建了一个简单的 WCF 服务应用程序(名为“DemoService”),它有一个 OperationContract
“Divide
”。
然后,我重构了所有服务并为默认文件命名为 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 服务,我将使用 **AddServiceReference** 菜单(右键单击 _References_ 文件夹)将 **Service Reference** 添加到 DemoClient
控制台应用程序。
现在,我们的 DemoClient
控制台应用程序已添加了 ServiceReference
。Program.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** 将具有以下结构:
.NET 异常对象
好的。现在让我们运行我们的应用程序。
为了启动 WCF 测试客户端来测试此服务,请将 DemoService
设置为启动项目,并在 **Solution Explorer** 中选择 _Service1.svc_ 或 _Service1.svc.cs_,然后开始调试。使用 WCF 测试客户端,我得到了如下示例输入的输出:
由于我们已经创建了一个控制台应用程序来消费我们的演示服务,我已经将 DemoClient
项目设置为启动项目。
运行应用程序后,我输入的值为零 (0)。
众所周知,除以零会引发异常,这里也发生了同样的情况,但我们得到的是什么异常?
"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."
从上面的错误消息来看,我们无法获得有关实际错误的线索。这个通用的错误消息会让客户端对服务引发的错误感到困惑。
正如通用错误消息所示,我们可以通过设置一个配置键来启用错误中的异常详细信息。
我已将 DemoService
的 web.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"
之前,我们得到了以下异常详细信息:
但在启用之后,我们在异常详细信息中获得了更多信息,如下所示:
异常是 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);
}
}
我还在 DemoClient
的 program.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);
}
}
完成服务端的更改后,请务必更新服务引用。
现在运行我们的 DemoClient
并输入零以测试 FaultContract
。
我们收到上述消息是因为我们还没有添加捕获 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.cs 的 try
/catch
部分添加了以下代码块。
catch (FaultException<DemoServiceReference.ValidationFault> e)
{
Console.WriteLine("Message: {0}, Description: {1}",
e.Detail.Message, e.Detail.Description);
client.Abort();
}
输出
倒带
FaultException 与 Exception
Exception
和 FaultException
不同。如果我们从 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 - FaultException
和 FaultContract
。希望您喜欢这篇文章,并从中获得了知识上的增值。
我已投入时间和精力来撰写我所有的文章,请不要忘记为您改进本文和未来文章的质量标记您的投票、建议和反馈。
历史
- 2016年1月29日:初始版本