一、传统的Exception Handling
我们沿用我们一直使用的Calculator的例子和简单的4层构架:
1. Service Contract- Artech.ExceptionHandling.Contract
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Contract
{
[ServiceContract]
public interface ICalculator
{
[OperationContract]
double Divide(double x, double y);
}
}
定义了一个单一的进行除法运算的Operation。
2. Service:Artech.ExceptionHandling.Service. CalculatorService
using System.Collections.Generic;
using System.Text;
using Artech.ExceptionHandling.Contract;
namespace Artech.ExceptionHandling.Service
{
public class CalculatorService:ICalculator
{
ICalculator Members
}
}
如果被除数是零,抛出一个DivideByZeroException Exception。
3. Service Hosting
Configuration:
< configuration >
< system .serviceModel >
< behaviors >
< serviceBehaviors >
< behavior name ="calculatorServiceBehavior" >
< serviceMetadata httpGetEnabled ="true" />
</ behavior >
</ serviceBehaviors >
</ behaviors >
< services >
< service behaviorConfiguration ="calculatorServiceBehavior" name ="Artech.ExceptionHandling.Service.CalculatorService" >
< endpoint binding ="basicHttpBinding" bindingConfiguration ="" contract ="Artech.ExceptionHandling.Contract.ICalculator" />
< host >
< baseAddresses >
< add baseAddress ="http://localhost:8888/Calculator" />
</ baseAddresses >
</ host >
</ service >
</ services >
</ system.serviceModel >
</ configuration >
Program
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using Artech.ExceptionHandling.Service;
namespace Artech.ExceptionHandling.Hosting
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost calculatorHost = new ServiceHost(typeof(CalculatorService)))
{
calculatorHost.Opened += delegate
{
Console.WriteLine("The Calculator service has begun to listen via the address:{0}", calculatorHost.BaseAddresses[0]);
};
calculatorHost.Open();
Console.Read();
}
}
}
}
4. Client
Configuration:
< configuration >
< system .serviceModel >
< client >
< endpoint address =http://localhost:8888/Calculator binding ="basicHttpBinding" contract ="Artech.ExceptionHandling.Contract.ICalculator"
name ="defualtEndpoint" />
</ client >
</ system.serviceModel >
</ configuration >
Program
using System.Collections.Generic;
using System.Text;
using Artech.ExceptionHandling.Contract;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Client
{
class Program
{
static void Main(string[] args)
{
ChannelFactory<ICalculator> calculatorFactory = new ChannelFactory<ICalculator>("defualtEndpoint");
ICalculator calculator = calculatorFactory.CreateChannel();
try
{
Console.WriteLine("Try to invoke Divide method");
Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 2, 0, calculator.Divide(2,0));
}
catch (Exception ex)
{
Console.WriteLine("An Exception is thrown.\n\tException Type:{0}\n\tError Message:{1}", ex.GetType(), ex.Message);
}
Console.Read();
}
}
}
把Service调用放在一个try/catch block中,看看Service端抛出的DivideByZeroException Exception能否被Catch。
我们运行这个程序,看看Client有怎样的输出:
我们发现Client catch住的不是我们Service端真正抛出的DivideByZeroException Exception,而是一个比较General的FaultException。Error message也是很general:
二、基于ServiceDebug的Exception Handling
很显然Client端Catch住的Exception对我们进行troubleshooting。为了利于我们进行有效的Debug,WCF提供了ServiceDebug Service Behavior。我们通过includeExceptionDetailInFaults属性设为true,那么如果Service抛出Exception,WCF会简单得包装这个Exception并把它置于Soap中Response到Service的访问者。介于此,我修改了Hosting的Configuration:
< configuration >
< system .serviceModel >
< behaviors >
< serviceBehaviors >
< behavior name ="calculatorServiceBehavior" >
< serviceMetadata httpGetEnabled ="true" />
< serviceDebug includeExceptionDetailInFaults ="true" />
</ behavior >
</ serviceBehaviors >
</ behaviors >
< services >
< service behaviorConfiguration ="calculatorServiceBehavior" name ="Artech.ExceptionHandling.Service.CalculatorService" >
< endpoint binding ="basicHttpBinding" bindingConfiguration ="" contract ="Artech.ExceptionHandling.Contract.ICalculator" />
< host >
< baseAddresses >
< add baseAddress ="http://localhost:8888/Calculator" />
</ baseAddresses >
</ host >
</ service >
</ services >
</ system.serviceModel >
</ configuration >
现在再次运行程序,看看现在的运行结果:
可以看到我们我们Catch的是一个FaultException< ExceptionDetail>Type的Exception,不是原来的FaultException。该Exception的Detail属性就是Service抛出的DivideByZeroException Exception。有兴趣的人可以自己测试一下。而且我们在Service端指定的Error Message也被Client获得。这种方式的Exception Handling方式确实比上面一种具有很强的指示性,对我们进行Debug确实很有帮助。但是这种方式确实不能正式用于我们最终发布的版本中,因为它会把Exception所有的信息返回到Client端,很容易泄露一些很敏感的信息。这也正是WCF把这个列入ServiceDebug Service Behavior的原因。
三、基于Fault Contract 的Exception Handling
既然上面通过定制ServiceDebug只能用于Debug阶段。我们必须寻求另外一种Exception Handling的方式。那就是我们现在将要介绍的基于FaultContract的解决方案。我们知道WCF采用一种基于Contract,Contract定义了进行交互的双方进行消息交换所遵循的准则和规范。Service Contract定义了包含了所有Operation的Service的接口,Data Contract定义了交互的数据的结构,而FaultContract实际上定义需要再双方之间进行交互的了异常、错误的表示。我们现在来看看如何来使用基于FaultContract的Exception Handling。
我们首先来定义一个表示Fault的类:MathError。考虑到这个类需要在Service 和Client使用,我把它定义在Artech.ExceptionHandling.Contract中:
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
namespace Artech.ExceptionHandling.Contract
{
[DataContract]
public class MathError
{
private string _operation;
private string _errorMessage;
public MathError(string operation, string errorMessage)
{
this._operation = operation;
this._errorMessage = errorMessage;
}
[DataMember]
public string Operation
{
get { return _operation; }
set { _operation = value; }
}
[DataMember]
public string ErrorMessage
{
get { return _errorMessage; }
set { _errorMessage = value; }
}
}
}
在MathError中定义了两个成员:表示出错操作的Operation和出错信息的ErrorMessage。由于该类的对象需要在Endpoint之间传递,所以必须是可序列化的,在WCF中,我们一般用两个不同的Serializer实现Object和XML的Serialization和Deserialization:Datacontract Serializer和XML Serializer。而对于Fault,只能使用前者。
定义了MathError,我们需要通过FaultContract将其运用到Service Contract中制定的Operation上面,我们通过下面的方式来实现:
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Contract
{
[ServiceContract]
public interface ICalculator
{
[OperationContract]
[FaultContract(typeof(MathError))]
double Divide(double x, double y);
}
}
我们在Divide上运用了FaultContract,并指定了封装了Fault对应的类型,那么最终这个基于MathError类型的FaultContract会被写入Service Description中,Client通过获取该Service Description(一般是获取WSDL),它就被识别它,就会将从接收到的Soap中对该Fault的XML Mapping到具体的MathError类型。
接着我们在Service Implementation中以抛出Exception的方式植入这个MathError对象:
using System.Collections.Generic;
using System.Text;
using Artech.ExceptionHandling.Contract;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Service
{
public class CalculatorService:ICalculator
{
ICalculator Members
}
}
在被除数为0的时候,抛出FaultException<MathError> Exception,并指定具体的MathError对象,以及一个FaultCode(一般指明出错的来源)和FaultReason(出错的原因)。
我们现在先不修改Client的Exception Handling的相关代码,先运行Hosting,看看WSDL中什么特别之处:
通过上面的Screenshot,我们可以看到,在PortType section中的Divide Operation定义了Message为tns:ICalculator_Divide_MathErrorFault_FaultMessage 的<wsdl:fault>节点。通过查看Message Section,我们发现tns:ICalculator_Divide_MathErrorFault_FaultMessage的Element为q1:MathError,该q1:MathError type实际上是被定义在一个XSD中,其Uri为http://localhost:8888/Calculator?xsd=xsd2,我们定义的所有DataContract都在其中,下面的整个内容:
< xs:schema elementFormDefault ="qualified" targetNamespace ="http://schemas.datacontract.org/2004/07/Artech.ExceptionHandling.Contract" xmlns:xs ="http://www.w3.org/2001/XMLSchema" xmlns:tns ="http://schemas.datacontract.org/2004/07/Artech.ExceptionHandling.Contract" >
< xs:complexType name ="MathError" >
< xs:sequence >
< xs:element minOccurs ="0" name ="ErrorMessage" nillable ="true" type ="xs:string" />
< xs:element minOccurs ="0" name ="Operation" nillable ="true" type ="xs:string" />
</ xs:sequence >
</ xs:complexType >
< xs:element name ="MathError" nillable ="true" type ="tns:MathError" />
</ xs:schema >
弄清楚了Fault在WSDL中表示后,我们来修改我们Client端的代码,来有效地进行Exception Handling:
{
ChannelFactory<ICalculator> calculatorFactory = new ChannelFactory<ICalculator>("defualtEndpoint");
ICalculator calculator = calculatorFactory.CreateChannel();
try
{
Console.WriteLine("Try to invoke Divide method");
Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 2, 0, calculator.Divide(2, 0));
}
catch (FaultException<MathError> ex)
{
MathError error = ex.Detail;
Console.WriteLine("An Fault is thrown.\n\tFault code:{0}\n\tFault Reason:{1}\n\tOperation:{2}\n\tMessage:{3}", ex.Code, ex.Reason, error.Operation, error.ErrorMessage);
}
catch (Exception ex)
{
Console.WriteLine("An Exception is thrown.\n\tException Type:{0}\n\tError Message:{1}", ex.GetType(), ex.Message);
}
Console.Read();
}
下面是运行后的输出结果:
WCF相关内容:
[原创]我的WCF之旅(1):创建一个简单的WCF程序
[原创]我的WCF之旅(2):Endpoint Overview
[原创]我的WCF之旅(3):在WCF中实现双向通信(Bi-directional Communication)
[原创]我的WCF之旅(4):WCF中的序列化(Serialization)- Part I
[原创]我的WCF之旅(4):WCF中的序列化(Serialization)- Part II
[原创]我的WCF之旅(5):Service Contract中的重载(Overloading)
[原创]我的WCF之旅(6):在Winform Application中调用Duplex Service出现TimeoutException的原因和解决方案
[原创]我的WCF之旅(7):面向服务架构(SOA)和面向对象编程(OOP)的结合——如何实现Service Contract的继承
[原创]我的WCF之旅(8):WCF中的Session和Instancing Management
[原创]我的WCF之旅(9):如何在WCF中使用tcpTrace来进行Soap Trace
[原创]我的WCF之旅(10): 如何在WCF进行Exception Handling
[原创]我的WCF之旅(11):再谈WCF的双向通讯-基于Http的双向通讯 V.S. 基于TCP的双向通讯
[原创]我的WCF之旅(12):使用MSMQ进行Reliable Messaging
[原创]我的WCF之旅(13):创建基于MSMQ的Responsive Service
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。