在开始吹牛之前,先说说.net Core的事情。
你不能把.NET Core作为全新体系来学习,因为它也是.NET。关于.NET Core,老周并不打算写什么,因为你懂了.NET,就懂了.NET Core了。使用.NET Core,你只需要学会一件事——学会如何配置环境,侧重点是运行环境,开发环境你可以用VS,只需要安装一个VS的Tool就行了,这个Tool目前仍是预览版,将来应该会内置到VS中。
不过你应该了解,不是所有的.NET API都会被移植,只有那些不太依赖于 Windows 平台的才可以移植,即核心API都是可以移植的,所以才叫Core。哪些API会依赖于Windows平台,老周告诉你一个非权威法则:凡是以 W 开头的,通常是不能移植的,比如 Windows Forms、WPF、WIF、WF、WCF等。WCF有一小部分已移植,以便于访问服务。此外,MEF也不能移植,因为MEF是专用于托管代码的,但微软已经提供了与MEF功能相同的本地代码集,你只需在Nuget中搜索 Microsoft.Composition 就能找到了。ASP.NET已移入ASP.NET Core。
CodeDOM用于生成代码的,应该与Windows平台依赖不大,将来有可能会被移植;同样用于代码生成的,还有LINQ的动态表达式树生成,这个平台依赖性也不强,将来应该会被移植。
其实Xamarin也不复杂,就是安装的东西比较多,毕竟它面向的是多个平台,你得准备好硬盘空间,当然,现在的硬盘,不用担心,动不动就是TB级别,几十个G算不了什么。Xamarin也有官方完整的文档,你只需了解类库API结构,直接调用即可,和.NET一样。另外,你应该通过文档学习各个平台的配置方法。
还是那句老话:只要你基础扎实,学什么都可以速成。唯独打基础不可以速成,那些打着速成口号的书都是骗人的。
好了,F话讲了,下面开始讲正题。今天咱们聊聊WCF中的操作选择器,或叫操作筛选器,这个是直接从Operation Selector翻译过来,直译就行了。反正从名字中就可以猜到,就是对服务操作进行选择的。
比如,有A、B、C三个服务操作,服务器收到客户端的调用消息,然而收到的消息会同时指向多个操作,这时候,就需要一个选择器,根据某些条件,决定执行哪个操作。
首先,大伙儿要弄清两个东东:服务协定与操作协定,服务协定是一个接口,而操作协定是接口中定义的一个方法。说白了,服务协定中可以包含1到n个操作协定。通常,协定用接口来定义,服务器需要实现接口,以完成处理,而客户端不需要,只要客户端知道协定的结构就行了。所以,协定用接口来定义的好处是可以与客户端共享代码,服务器上的实现不应该向客户端公开。
你也可以这样。直接把服务器上的服务实现类作为服务协定,然后,客户端重新定义一个接口作为协定,只要协定的命名空间和名字,以及操作的action和replyaction匹配就行。
这里所说的协定命名空间不是代码的命名空间,而是SOAP中用的命名空间,即XML命名空间。
正常情况下,操作选择器并不需要,因为每一个服务操作都会有唯一的Action和ReplyAction值,这是不能重复的,在通信的时候,这些值会对应于SOAP消息的action头,因此每条SOAP消息都能与对应的操作绑定。
可是,如果:
1、在服务器上存在Action相同的操作协定。
2、客户端上只有一个操作协定。
这种情形下,就出大事了,SOAP带着客户端的action,跋山涉水,不远万里来到服务器,本来action说是要找王老板的,结果到了服务器那里居然发现有N个王老板,而且长得一模一样,该不会是同胞孪生兄弟吧。这时候就不知道要找哪位王老板了,那得请人来分别一下,到底哪个才是要找的人,这个帮手可以是他们的父母,也可以是熟悉他们的亲友。这个帮手就是Operation Selector。
来,看看。
下面是服务协定的声明:
[ServiceContract(Namespace = "demo", Name = "runner")] public interface IService { [OperationContract(Name = "run_gen", Action = "run", ReplyAction = "runback")] ReplyMessage RunGen(); [OperationContract(Name = "run_admin", Action = "run", ReplyAction = "runback")] ReplyMessage RunAdmin(); }
这个服务协定的命名空间为demo,名字叫runner,有没有发现它有什么不对劲?是了,你这么细心,肯定看出来了,它包含两个操作协定,虽然操作协定的名字不同,可是它的Action和ReplyAction是相同的。SOAP消息是通过action来定位操作的,现在两个操作的action相同,就会难以定位了。
而在客户端,只有一个操作协定。
[ServiceContract(Namespace = "demo", Name = "runner")] public interface ITestChannel : IClientChannel { [OperationContract(Action = "run", ReplyAction = "runback")] ReplyMessage Run(); }
注意,命名空间demo与服务协定名runner要与服务器端的一致。ReplyMessage是一个消息协定,这里使用消息协定,是为了让SOAP消息正文在序列化和反序列化时能够有相同的节点元素,否则无法完成反序列化。因为默认情况下,消息正文的元素是操作协定的名字,然而在上面的情形中,服务器端和客户端上的操作协定名字不同,如果这样序列化,那么另一端是无法反序列化的,这样会导致通信失败。为了让消息正文能有相同的封装元素,此处可以使用消息协定(用数据协定也可以,只要保证正序列化后的XML结构相同即可)。
[MessageContract(WrapperName = "replymsg")] public class ReplyMessage { [MessageBodyMember(Name = "display")] public string Display { get; set; } [MessageBodyMember(Name = "score")] public int Score { get; set; } }
WrapperName就是消息正文的封装元素名。
当客户端调用Run方法时,SOAP消息的action为run,但是服务器上有两个action相同的操作,所以运行后会发生这样的错误。
显然,action相同的服务操作是不相容的。
这个时候,就需要对要调用的服务操作进行一下选择了,我这个例子的功能是这样的,客户端在调用服务时,会用用户名和密码登录,如果是管理员就可以获得1000积分,如果是一般用户就获得500积分。
要对服务操作进行选择,需要实现 IDispatchOperationSelector 接口,凡是有 Dispatch 字样的东东都是用于服务端的,比如 IDispatchMessageInspector,用来拦截服务器端消息的;凡是带有 Client 字样的,都是用于客户端的,比如,IClientMessageFormatter ,用于在客户端对消息进行自定义序列化处理的。这样部件全都位于 System.ServiceModel.Dispatcher 命名空间下。
IDispatchOperationSelector 有一个 SelectOperation 方法,方法返回的是一个字符串,表示要执行的操作,注意,这个操作的名字不一定是操作方法的名字,而是 OperationContractAttribute.Name 的值,如果 Name 值没有指定,它默认是方法的名字。在上面例子中,服务器端的操作名字分别为run_gen和run_admin。
好,现在,咱们来筛选一下。
public class MyOperationSelector : IDispatchOperationSelector { public string SelectOperation(ref Message message) { IIdentity id = message.Properties.Security.ServiceSecurityContext.PrimaryIdentity; var user = UserValidation.User.GetUserFromName(id.Name); if (user != null && user.Type == UserValidation.UserType.Admin) { return "run_admin"; } return "run_gen"; } }
Selector定义完了,那怎么用呢,把它赋值给 DispatchRuntime 类的 OperationSelector 属性即可。要完成这一过程,就得实现自定义的behavior了,behavior(翻译为 行为)可用来扩展WCF功能的一个接入点。
按照扩展的层次,可以分为 service behavior、endpoint behavior、contract behavior、operation behavior。
向service host加入behavior扩展一定要在open之前,如果服务已经运行,修改是没有用的。而且,在扩展时,一定要遵循对应法则,比如,你要扩展的行为和终结点有关的,最好实现endpoint behavior,如果和服务有关的,最好实现service behavior,这样做可以防止意外发生。因为服务体系在初始化过程中是从上到下,从外到内,从大到小的,即服务先初始化,然后再初始化终结点(通道层也会初始化),然后初始化服务协定,最后初始化操作协定、参数等。
比如,我们这里要对操作协定进行筛选,不应该在服务和终结点上扩展,因为这样做,很容易出现我们自定义的behavior所做的处理,被其他behavior覆盖的情况,例如,我实现终结点behavior,并为服务协定分配自定义的Formatter,我们虽然设置了Formatter,但要是别的behavior也修改了Formatter,那我们的扩展代码就白做了。更不应该在通道层扩展,也不能在操作协定上扩展,因为操作协定上扩展behavior只能处理单个操作协定,而我们这里是要在多个操作协定中选择一个来执行的,所以,最合理的扩展点是在服务协定这一层。
因此,本例应该实现 IContractBehavior,以便在服务协定层上进行扩展。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)] public class MyContractBehaviorAttribute : Attribute, IContractBehavior, IContractBehaviorAttribute { public Type TargetContract { get { return typeof(ServerContracts.IService); } } public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime) { dispatchRuntime.OperationSelector = new MyOperationSelector(); } public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { } }
本类的重点是实现 IContractBehavior 接口,该接口要求实现四个方法,其中 AddBindingParameters 和 Validate 方法一般不用处理;ApplyClientBehavior 是当客户端的服务协定应用该behavior时进行处理,在本例中,只需要扩展服务端的行为,与客户端无关,故无需实现该方法;ApplyDispatchBehavior方法表示的是服务器端服务协定应用behavior时进行处理,在本例中,正是通过实现该方法,把自定义的 MyOperationSelector 对象赋值给 dispatchRuntime.OperationSelector 属性。
另外,大伙也看到,本类还继承了 Attribute类,那是为了方便应用,如果不将该Behavior声明为特性类,那就需要从serviceHost的Descroption中获取对服务协定的描述Contract Description,然后再把该 MyOperationSelector 对象插入到 Behaviors集合中;不过,本类已将其声明为attribute,那么,直接把它应用到服务类上就可以了,WCF运行时会自动把它塞进Contract Behaviors 集合中。
IContractBehaviorAttribute 是一个很好玩的接口,它有一个 TargetContract 属性,让你返回一个Type,这个Type是服务协定的Type,通常是接口类型。
由于服务类有可能实现N个服务协定(记得.NET类型特点吧,一个类可以实现多个接口),而这个 TargetContract 属性就是一个限制,它限制我定义的这个 MyContractBehaviorAttribute 特性只能应用到 TargetContract 所指定的协定上,而其他协定则被忽略。
假如,一个服务类MyService,实现了两个服务协定,分别是 IMoneyA 和 IMoneyB,随后我把 MyContractBehaviorAttribute 应用到 MyService 上,并且让 TargetContract 属性返回 typeof(IMoneyA),这样一来,该特性只对 IMoneyA 协定起作用,而 IMoneyB 协定无影响。
如果不实现 IContractBehaviorAttribute 接口,那么,MyContractBehaviorAttribute 就会应用到服务类所实现的所有服务协定上。
这么解释,你应该能听懂吧。
OK,现在好了,有了操作选择器,尽管服务协定中存两个 Action 相同的操作协定,但不会发生错误了,因为选择器只能选择一个操作来调用。
下面在客户端测试一下,假设用户 user1 是管理员, user2 是一般用户,分别以两个用户来调用服务。
var fact = new ChannelFactory<ClientContracts.ITestChannel>("client"); fact.Credentials.UserName.UserName = "user1"; fact.Credentials.UserName.Password = "1234"; var ch = fact.CreateChannel(); var reply = ch.Run(); Console.WriteLine($"消息:{reply.Display},积分:{reply.Score}"); ch.Close(); fact.Close(); fact = new ChannelFactory<ClientContracts.ITestChannel>("client"); fact.Credentials.UserName.UserName = "user2"; fact.Credentials.UserName.Password = "1234"; ch = fact.CreateChannel(); reply = ch.Run(); Console.WriteLine($"消息:{reply.Display},积分:{reply.Score}"); ch.Close(); fact.Close();
ChannelFactory一旦开启通道之后,它的各种属性都会被锁定,不能再修改,因此,当切换到 user2 用户调用时,只能重新new一个ChaanelFactory实例。
运行结果如下图所示:
这时候你看到了,操作选择器起作用了。
好了,今天的话题就讨论到这里吧。不要认为WCF很难学,其实很简单的,你可以暂时放下那些复杂的概念,而从实际应用的角度去学习,至于那些概念嘛,等什么时候你有兴趣了,再回过头去细细研究。
所以,WCF不难学,别担心,看了老周这些烂文之后,你就明白了。