【.net深呼吸】(WCF)OperationContextScope 的用途

一个WCF服务可以实现多个服务协定(服务协定实为接口),不过,每个终结点只能与一个服务协定关联,并指定调用的唯一地址。那么,binding是干吗的?binding是负责描述通信的协议,以及消息是否加密等内容。

好,不扯F话,说说今天的主题——OperationContextScope,这是一个类,而且是实现了 IDisposable 接口,说明这个类在实例化后,可能会持有某些特定的状态信息,在释放实例时需要进行清理。

这个猜测很对,OperationContextScope类的作用其实就是这样。说具体一点,就是在这个类实例化后,到它被释放之间形成一个代码范围,在这个特定的范围内,可以对正在调用的服务操作进行访问,最典型的做法是在这个范围内修改消息头,通常是添加自定义的消息头。

那为什么要用OperationContextScope来圈定一个范围来修改消息头呢?因为这样做可以保证只有在这一次调用服务操作才会添加自定义的消息头,在其他地方调用则不会添加自定义头。

一般来说,自定义消息头是用来附加一些额外的信息,这些数据不属于服务操作正文部分,并且是可有可无的。有点像发电子邮件时的附件,附件是可有可无的,可以与邮件正文有关,也可以与正文无关。

好,理论说完了。WCF相关的文章,我之所以写得少,就是因为它难写,WCF本身就涉及到很多Web服务相关的标准和概念,理解起来也不容易,而理论部分总是让人越看越不懂。经过老周K年时间的摸爬滚打,总结出来最有用的编程学习办法就是——写代码。虽然听起来是句F话,但是,真的找不到比这一招更好的办法。就拿老周学习WCF的过程来说吧,尽管许多概念可以网上查,可是看了之后呢,懂吗?还是不懂,哪些读了与Web服务相关的专著,还是不懂;哪怕再看一遍MSDN,似乎还是不懂。那怎么办,无他,硬着头皮敲代码。理论方面的东西弄不懂,难道连代码也不会写了不成?嘿,这果然是个好招儿,本来不懂的东东,把代码一写,果然就有感觉了。

道理一样,要搞懂OperationContextScope类是个什么货色,光用嘴说太抽象,但是,把代码往VS里面一写,我相信你会马上懂了。不信?咱们试试。

按照正常人类思维方式,我们应当先做服务器端。

先弄个服务协定,当然,它是一个接口,这个基础相信大家是有的。

    [ServiceContract(ConfigurationName ="ct",Namespace ="http://dog.org", Name ="哈吧dog")]
    interface ITest
    {
        [OperationContract(Name = "wang")]
        string SaySomthing(string name);
    }

服务操作很简单,传入一个字符串,返回一个字符串。哦,对了,这里有个玩意儿可能大家比较陌生,ConfigurationName ="ct" 是个什么鬼?首先,我声明一下,它不是鬼;再者,它呢,你可以随便指定一个名字,最好是简短的,方便你记住的,因为等会儿在写配置文件时有用,这里我取了个名字叫ct。

然后,理所当然,就是实现服务协定。

    [ServiceBehavior(ConfigurationName = "sv")]
    public class Service : ITest
    {
        public string SaySomthing(string name)
        {
            Console.WriteLine("
========= 操作被调用 ===========");
            OperationContext context = OperationContext.Current;
            var hds = context.IncomingMessageHeaders;
            StringBuilder strb = new StringBuilder();
            foreach (var h in hds)
            {
                strb.AppendLine($"【{h.Namespace} - {h.Name}】: {hds.GetHeader<string>(h.Name, h.Namespace)}");
            }
            Console.WriteLine(strb);
            return $"旺旺!{name}。";
        }
    }

在实现 SaySomething 方法过程中,我做了些手脚,通过OperationContext的InComingMessageHeaders属性得到了客户端发送过来的消息的Header列表,然后在屏幕上输出每个Header的信息,包括命名空间,名称,以及内容。Header的内容可以通过GetHeader<T>方法来获取,T是返回内容的类型,如果希望把header的内容以字符串形式返回,就指定string。

和刚才服务协定定义相似,可能大家又看到了,我在类上应用了ServiceBehavior特性,又搞了个ConfigurationName,它的名字叫 sv, 同样,也是在配置文件上使用的。

最后,创建ServiceHost,并打开服务。

            using (ServiceHost host=new ServiceHost(typeof(Service)))
            {
                try
                {
                    host.Open();
                    Console.WriteLine("服务已启动。");
                }
                catch(Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                Console.Read();
            }

一般在演示示例时,我不用IIS来承载,麻烦,直接弄个控制台应用程序来启动服务,方便。

下面,开始配置一个服务的配置文件。

  <system.serviceModel>
    <services>
      <service name="sv">
        <endpoint address="http://127.0.0.1:1000/mt" contract="ct" binding="basicHttpBinding" />
      </service>
    </services>
  </system.serviceModel>

看出来了没,现在知道我刚才搞的两个ConfigurationName的作用了吧。没看出来?我给你讲讲。

先看 service 元素,通常,name应该指定服务类的全名,包括命名空间,比如我刚刚的类名为Service,这里应写上 name = "MyNamespace.Service",不过,因为我刚才用ServiceBehaviorAttribute指定了ConfigurationName叫 sv,所以在配置文件中,我不用再写一大串的类型名称了,只写上 sv 就完事了。

一样的道理,endpoint是必须与一个服务协定关联的,刚才在定义服务协定时,我也用ConfigurationName给了一个名字叫 ct,故这里的 contract = "ct" 就是指向ITest协定接口了,如果不指定ConfigurationName,那么在配置endpoint元素时,你只好把接口的路径写全了,即contract = "MyNamespace.ITest",但这里我可以轻松写成ct就可以了。

Good,服务端完成,现在做客户端。先给客户端的配置文件写一下。

  <system.serviceModel>
    <client>
      <endpoint name="ep" address="http://127.0.0.1:1000/mt" contract="ct" binding="basicHttpBinding"/>
    </client>
  </system.serviceModel>

然后,在客户端代码中重定义服务协定,你可以让服务器和客户端共享服务协定,当然也可以重新定义,接口和成员方法的名字可以不同,只要参数类型,个数和顺序对上就行。当然了,协定的命名空间和名称要与服务端一致。

    [ServiceContract(Namespace = "http://dog.org", Name = "哈吧dog", ConfigurationName = "ct")]
    interface IDemo : IClientChannel
    {
        [OperationContract(Name = "wang")]
        string SpeakTo(string name);
    }

这里我让它继承了IClientChannel接口,因为待会儿要用。在客户端,只定义接口就行了,不用实现,运行时库会自动完成。你也不用实现IClientChannel接口,因为WCF内部已经有实现类了。

接着,用ChannelFactory<IDemo>来创建通道,通道类型就是协定类型接口。

        ChannelFactory<IDemo> fac = null;
        IDemo channel = null;

             …………
        fac = new ChannelFactory<IDemo>("ep");
        channel = fac.CreateChannel();

       // 调用完后,记得X掉它们
       channel?.Close();
       fac?.Close();

调用CreateChannel方法就能得到实现了IDemo接口的实例,这个内部会自动完成,你可以不管。在调用Close方法时,变量名后面多了个?,这是C# 6的新玩法,意思就是如果变量引用的是null,那代码就不执行了。

现在,我们用同一个通道实例,对服务进行两次调用。

            using (OperationContextScope scope = new OperationContextScope(channel))
            {
                // 获取当前操作上下文
                OperationContext context = OperationContext.Current;
                // 添加新的消息头
                MessageHeader hd = MessageHeader.CreateHeader("add_msg", "http://www.dog.net", "这是一条德国进口犬");
                context.OutgoingMessageHeaders.Add(hd);
                // 调用服务操作
                lblMsg.Text += channel.SpeakTo("杰克") + "
";
            }
            // 再次调用
            lblMsg.Text += channel.SpeakTo("肯肯");

第一次调用,是在OperationContextScope所划定的范围内进行的,并且向消息添加了个自定义Header;而第二次调用是在Scope范围之外的。

两次调用后,看看服务器的输出内容,你就能发现什么新事情了。

看出来了吧,第一次调用多了个Header,而第二次调用没有。看看两次发出的消息。

回忆一下,我们刚才两次调用服务,用的是同一个通道实例——channel,它在第一次调用时有自定义hd,而第二次调用时,自定义hd不见了。

从这个例子的比较中,你就知道OperationContextScope的用途了。Scope所圈定的范围内所做的修改只在该范围内有效,当跳出这个Scope,你再调用服务,Operation的状态会自动被还原。

在Scope中调用,给消息加了自定义的头,但只在这个范围内有效,等代码走出Scope后,对Operation的调用就会自动变回原来的样子,所以,自定义的头部不复存在。正因为如此,在服务器上的输出中,第一次调用会有自定义头,而第二次没有自定义头。第一次调用时,客户端在Scope中添加了自定义头,而第二次调用是在Scope之外,消息状态被还原,自定义头就不见了。

老周只能讲到这儿了,能不能懂,真的看你的理解了,前面都说了,WCF的东西真的很难讲解。如果对OperationContextScope还不理解,可以把示例反复研究一下,示例中我调用了两次服务作对比,如果你不理解,可以改代码,让客户端调用三次、四次。

示例代码下载地址

原文地址:https://www.cnblogs.com/tcjiaan/p/5635141.html