【WCF】操作选择器

作者: 李禾

更新时间:2022-03-26 13:10:36

3083 阅读

在开始吹牛之前,先说说.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不难学,别担心,看了老周这些烂文之后,你就明白了。

 

示例代码下载

 

版权声明:本文著作权归作者【李禾 】所有,不代表本网站立场。

侵权请联系:root_email@163.com

相关推荐