[WCF]终结点与服务寻址(四)
本文是终结点与服务寻址系列的第四篇文章。
4.使用自定义寻址报头实现定制寻址
在这个实验中,我将会为服务终结点定义一个自定义的报头(更多书叫做首部),这个报头用于消息寻址,或者说筛选不符合报头的消息。一个服务调用的请求消息需要经过两重终结点筛选器的筛选才能最终被终结点接受,这两个筛选器分别是寻址筛选器和服务契约筛选器,而这个实验就是讲述前者。
这个实验中,服务宿主程序的代码也是没什么改变,依然使用实验2中的服务宿主程序代码,但服务端的配置文件就不一样了,以下是其代码:

<service name="WCF_Study1.ServiceContracts.SystemInfoService"
behaviorConfiguration="metaBehavior">
<endpoint address="SystemInfoService"
binding="basicHttpBinding"
contract="WCF_Study1.ServiceContracts.ISystemInfoServiceContract">
<headers>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v1</UserType>
</headers>
</endpoint>
<endpoint address="SystemInfoServiceV2"
binding="basicHttpBinding"
contract="WCF_Study1.ServiceContracts.ISystemInfoServiceContract">
<headers>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v2</UserType>
</headers>
</endpoint>
<host>
<baseAddresses>
<add baseAddress="http://localhost:8000"/>
</baseAddresses>
</host>
</service>
</services>
我在配置代码中部署了两个终结点,而且两个终结点分别在其内的<headers></headers>节点中定义了一个<UserType />节点,也就是这个实验的主题——自定义报头。该报头中还设置了命名空间属性,并以Custom Address Header v1和Custom Address Header v2作为其报头正文。解释这个自定义报头的作用之前,先看一下客户端配置文件的代码:

<endpoint address="http://localhost:8000/SystemInfoService"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_ISystemInfoServiceContract"
contract="Services.ISystemInfoServiceContract"
name="BasicHttpBinding_ISystemInfoServiceContract">
<headers>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v1</UserType>
<!--
当我不设置上述自定义寻址报头时,在调用服务后则会在服务端触发异常,并把异常回送到客户端。
从消息中可以看到首部正如上述定义一样。
-->
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v1.1</UserType>
<!--
尽管服务端终结点没有使用上面的新寻址报头,但将此报头添加到客户端终结点配置中,依然能够正
常调用服务。
-->
</headers>
</endpoint>
<endpoint address="http://localhost:8000/SystemInfoServiceV2"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_ISystemInfoServiceContract"
contract="Services.ISystemInfoServiceContract"
name="BasicHttpBinding_ISystemInfoServiceContractV2" >
<!--
上面的第二终结点,在服务端,对应的终结点已经配置了自定义寻址报头,而我不打算在配置文件中进行同样的配置,
我将实验使用代码来灵活地添加寻址报头
-->
</endpoint>
</client>
正如在配置代码中的注释说明一样,通过在客户端中节点中添加一致的自定义报头,那么请求服务的消息就能成功被服务端终结点接受,我通过WCF消息追踪器摘取了消息的内容,以验证自定义报头的确出现在消息中,
当我仅适用Custom Address Header v1这个报头时:

<s:Header>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v1</UserType>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:8000/SystemInfoService</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://www.dreamofflea.net/ISystemInfoServiceContract/GetServerMachineName</Action>
</s:Header>
<s:Body>
<GetServerMachineName xmlns="http://www.dreamofflea.net" />
</s:Body>
</s:Envelope>
当我同时使用Custom Address Header v1.1这来年改革自定义报头时:

<s:Header>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v1</UserType>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v1.1</UserType>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:8000/SystemInfoService</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://www.dreamofflea.net/ISystemInfoServiceContract/GetServerMachineName</Action>
</s:Header>
<s:Body>
<GetServerMachineName xmlns="http://www.dreamofflea.net" />
</s:Body>
</s:Envelope>
无论是否附加其他自定义报头,只要设置了与服务端终结点一致的自定义报头,服务都能正常被调用(附加是为了验证其不影响结果)。那如果客户端终结点不设置一个匹配的自定义报头会怎样呢?观看以下异常:
在刚才客户端配置代码中,有一个名为BasicHttpBinding_ISystemInfoServiceContractV2的客户端终结点,那就是我接戏来实验的内容——在客户端程序中以代码的方式动态为服务调用的请求信息添加自定义报头。这种方式的一个优点就是灵活,当我在客户端配置文件中设置好自定义报头时,每个通过该客户端终结点发出的消息都一定会包含该报头,相反在代码中动态追加报头就不会有这样的约束。留意以下客户端程序定义的方法代码:

{
WCF_Study1.Client.Services.SystemInfoServiceContractClient proxy
= new WCF_Study1.Client.Services.SystemInfoServiceContractClient("BasicHttpBinding_ISystemInfoServiceContractV2");
Console.WriteLine("调用的终结点逻辑地址:{0}", proxy.Endpoint.Address.ToString());
AddressHeader ad = AddressHeader.CreateAddressHeader("UserType", "http://www.dreamofflea.net", "Custom Address Header v2");
using (OperationContextScope scope = new OperationContextScope(proxy.InnerChannel))
{
OperationContext.Current.OutgoingMessageHeaders.Add(ad.ToMessageHeader());
Console.WriteLine("调用结果:Remote Server Name:{0}", proxy.GetServerMachineName());
}
/*
* 利用操作上下文,动态地为发出的消息添加寻址报头,这种方式可以防止每次发送消息都自动添加
* 报头,而是在需要的时候才添加。不过缺点就是要把报头信息硬编码在代码中,不过也可以在配置文件
* 中保存报头信息。
*/
Console.WriteLine();
}
在方法中,我首先创建了一个寻址报头(AddressHeader)对象,请注意它的创建方式。在WCF对象模型中,大多数方法都是接受MessageHeader作为参数的,因为MessageHeader代表通用的报头。因此我必须把AddressHeader转换成MessageHeader,而AddressHeader的ToMessageHeader方法正是我所需要的。接下来我创建了一个操作上下文的范围,并使用当前的操作上下文为所有发出的消息追加刚才创建的寻址报头。关于操作上下文的知识,将在往后的系列文章中讲述。以下同样是发出的消息内容:

<s:Header>
<UserType xmlns="http://www.dreamofflea.net">Custom Address Header v2</UserType>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:8000/SystemInfoServiceV2</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://www.dreamofflea.net/ISystemInfoServiceContract/GetServerMachineName</Action>
</s:Header>
<s:Body>
<GetServerMachineName xmlns="http://www.dreamofflea.net" />
</s:Body>
</s:Envelope>