【WCF】错误协定声明
在上一篇烂文中,老周给大伙伴们介绍了 IErrorHandler 接口的使用,今天,老周补充一个错误处理的知识点——错误协定。
错误协定与IErrorHandler接口不同,大伙伴们应该记得,上回我们是把自己实现IErrorHandler接口的类型添加到ChannelDispatcher中的,也就是说,IErrorHandler处理的是通道层的错误,它可以捕捉到多个服务操作上发生的错误。而错误协定是面向协定层的,它是通过 FaultContractAttribute 来声明的,这个特性在上一篇文章中我们用过,它应用的目标是方法。故而这个特性是针对某个服务操作而定义的,主要指明在某个服务操作可能会引发的 FaultException<TDetail> 异常,其中的TDetail泛型参数的类型是在FaultContract特性中指定。
在上一篇烂文中提到过,如果是像string、int这些基本类型,是可以直接用的,如果是自定义的类型,则应该将其声明为数据协定,然后在FaultContract特性中作声明。
咱们来实战一下。首先,咱们定一个服务协定。
[ServiceContract(Namespace = "zhou-demo", Name = "runner")] public interface IDemo { [OperationContract(Action = "getNum", ReplyAction = "getNumRes")] [FaultContract(typeof(ErrorData), Namespace = "zhou-demo/faults", Name = "fault-ct", Action = "fault-back")] int GetNumber(); }
这个协定没什么重要的事情干,无非就是返回一个整数,待会儿咱们让它返回随机整数。
不过大家注意,我在方法上应用了FaultContract特性,它的构造函数是个Type,这个Type用来指定存放错误详细信息的类型,这个类型就是FaultException<TDetail>中的TDetail的类型。其他属性如Action,Namespace、Name这些,不用我多说了吧,都知道干吗用的,你也可以不设置的,它会自动生成一个默认值。
ErrorData是我自己定义的一个数据协定,请看下面代码。
[DataContract(Namespace = "zhou-demo/fault-data")] public class ErrorData { [DataMember] public int ErrorNumber { get; set; } [DataMember] public string ErrorSource { get; set; } [DataMember] public string ErrorMessage { get; set; } [DataMember] public string ErrorType { get; set; } }
这个好懂吧,不必介绍了吧。
当服务操作中发生异常时,可以用ErrorData类来封装自定义的错误信息,WCF会把它序列化,然后塞进SOAP消息中发回给客户端,客户端就可以catch到这些错误数据了。
接下来,实现服务类。
class DemoService : IDemo { public int GetNumber() { Random rand = new Random(); int n = rand.Next(2000); if(n % 3 == 0) { ErrorData erdata = new ErrorData { ErrorNumber = n, ErrorSource="人品", ErrorType="生物机能错误", ErrorMessage ="运气指数不佳,引发不可修复错误。" }; throw new FaultException<ErrorData>(erdata); } return n; } }
这代码没什么技术含量,你如果看不懂,应该写一份30万字的检讨书。随机生成一个整数,如果这个整数可以被3整除,那就抛出异常,否则正常返回这个数值。
服务完成,下面该轮到实例化ServiceHost了,有了Host我们才能调用服务。Host的处理很TMD简单,直接指定:基址 + 服务类型 = 完事。
Uri baseAddress = new Uri("http://localhost:9973"); ServiceHost host = new ServiceHost(typeof(DemoService), baseAddress); host.Open(); Console.WriteLine("服务已启动。"); …… host.Close();
还记得老周以前说过吧,如果仅仅指定基址,而不定义任何EndPoint的话,Host会自动根据协定个数和基址个数,自动生成默认的终结点。在这个高大上例子中,基址是http方案的,所以生成的终结点默认使用BasicHttpBinding,由于服务类只实现了一个服务协定接口,所以该Host中也就只有一个终结点了。
好了,万事具备,只欠东风了,东风就是客户端调用代码。
Console.WriteLine("请按 Esc 键退出,按任意键调用服务。"); ConsoleKeyInfo keyinfo; while((keyinfo = Console.ReadKey(true)).Key != ConsoleKey.Escape) { EndpointAddress epaddr = new EndpointAddress(baseAddress); BasicHttpBinding binding = new BasicHttpBinding(); IDemo channel = ChannelFactory<IDemo>.CreateChannel(binding, epaddr); try { int x = channel.GetNumber(); Console.WriteLine($"\n调用结果:{x}。"); } catch(FaultException<ErrorData> ftex) { ErrorData detail = ftex.Detail; string msg = $"\n错误数字:{detail.ErrorNumber}\n" + $"错误源:{detail.ErrorSource}\n" + $"错误类型:{detail.ErrorType}\n" + $"错误详情:{detail.ErrorMessage}"; Console.WriteLine(msg); } catch(FaultException fex) { Console.WriteLine(fex.Message); } catch(Exception ex) { Console.WriteLine(ex.Message); } finally { IClientChannel client = (IClientChannel)channel; client.Close(); }
为了能够在控制台应用程序中多次调用服务(整数是随机生成,不能保证每次都抛异常),老周做了些处理,按任意键来调用服务,当然不能弄成死循环,为了使循环不死,我留了个出口——按Esc键。
如果在服务器上果真引发了FaultException<TDetail>,那么,在客户端就可以捕捉到该异常。服务器引发异常后会数据对象序列化,传到客户端后,会进行反序列化。
示例运行结果如下图所示。
好了,知识点补完了,老周得准备吃lunch了,今天是国庆长假第二天,下午老周要去朋友H家里看现场水墨绘画,据说有不少妹子在那里。
===================================================
很久没讲故事了,这回就讲一讲老周为什么会进入IT界吧。
其实原因也极其简单,也不是什么远大志向,老周之所以会进入IT领域,就是因为喜欢编程,纯粹就是爱编程,不等于爱做项目,老周很讨厌做项目。
还是孔爷爷说得好,“知之者不如好之者,好之者不如乐之者”,对老周而言,除了卖保险之外,什么行业都可以进,要不是热爱编程,老周是不会选择走IT道路。而编程里面,犹爱.NET。
所以,以后你要是想跟老周讨论编程问题,那就只讨论编程上的事,不要跟我讨论怎么找到好工作,找工作那是你的事,跟我没关系,也不要讨论.net有什么前景之类的话题,那是匹夫才讨论的问题。不过,最近些天,好像又有些不学无术的人,又在搞.NET与Java之争,那些东西在老周眼里,等同于看一场相声表演而已。
反正老周只告诉你一句话:我就是热爱.NET。
如果你喜欢.NET,欢迎你看看老周写的烂文章;如果你不喜欢.NET,那无所谓,你可以无视老周的博客。但是,老周不允许有人在博客评论里面说不文明的言论。心诚则与君交,心不诚则与君绝。