2.1.3 如何指定AddressHeader
WCF是一个基于消息的通信框架,具有一个消息处理的管道。消息流到该管道不同的位置,要对消息进行相应的处理。虽然基于REST/POX的Web Service越来越盛行,但是基于SOAP的Web Service还是主流。SOAP已经成为了Web Service事实上的标准,SOAP同时也是整个WS-*体系结构的基础。得益于可扩展的SOAP消息结构:SOAP报头+消息主体,我们可以把各种各样的控制信息置于SOAP封套(Envelope)的报头集合中,各种WS-* 协议(比如WS-Addressing、WS-Reliable Messaging、WS-Security等)通过定义各自的SOAP报头得以实现。
对于基于消息的通信方式来说,寻址(Addressing)是需要首先解决的问题。一般情况下,WCF通过WS-Addressing规定的标准解决寻址的问题。也就是说通过WS-Addressing标准的SOAP报头内容对目标地址进行解析。典型的寻址相关的报头包含:<wsa:To>、<wsa:Via>、<wsa:ReplyTo>等。
但是在某些情况下,默认的这种寻址方式不能完全解决现实中的具体需求,我们需要一种不同的方式对消息进行路由,在这种情况下,需要实现手工寻址(Manual Addressing)。对于实现手工寻址所需相关的辅助信息,需要定义在SOAP消息的报头中。WCF通过一个特殊的对象,AddressHeader来表示这种寻址相关的SOAP消息报头。
AddressHeader和AddressHeaderCollection
AddressHeader定义在System.ServiceModel.Channels命名空间下,表示用于消息寻址相关的信息的报头,下面是AddressHeader的简单定义:
public abstract class AddressHeader { //其他成员 public static AddressHeader CreateAddressHeader(object value); public static AddressHeader CreateAddressHeader(object value, XmlObjectSerializer serializer); public static AddressHeader CreateAddressHeader(string name, string ns, object value); public T GetValue<T>(); public T GetValue<T>(XmlObjectSerializer serializer); public MessageHeader ToMessageHeader(); public void WriteAddressHeader(XmlDictionaryWriter writer); public void WriteAddressHeader(XmlWriter writer); public void WriteAddressHeaderContents(XmlDictionaryWriter writer); public void WriteStartAddressHeader(XmlDictionaryWriter writer); public abstract string Name { get; } public abstract string Namespace { get; } }
通过AddressHeader的静态方法CreateAddressHeader可以很方便地创建AddressHeader对象;GetValue方法用于获取AddressHeader的值;通过ToMessageHeader可以将AddressHeader转化为MessageHeader对象,而MessageHeader对象表示的就是一个消息的报头。4个WriteXXX方法将AddressHeader写入一个流或具体的文件中。Name和Namespace属性代表的是报头的名称和命名空间。
除了AddressHeader类型,在System.ServiceModel.Channels命名空间下还定义了另一个与之相关的对象AddressHeaderCollection,它表示AddressHeader的集合。AddressHeaderCollection继承自ReadOnlyCollection<AddressHeader>,所以该集合是只读的。
public sealed class AddressHeaderCollection:ReadOnlyCollection<AddressHeader> { //其他成员 public AddressHeaderCollection(); public AddressHeaderCollection(IEnumerable<AddressHeader> addressHeaders); public void AddHeadersTo(Message message); public AddressHeader[] FindAll(string name, string ns); public AddressHeader FindHeader(string name, string ns); }
通过AddHeadersTo方法可以很容易地将一个AddressHeaderCollection对象添加到一个Message对象的报头列表中;FindAll和FindHeader根据报头的名称和命名空间找到对应的AddressHeader。FindAll得到所有相关的AddressHeader,而FindHeader只获得满足条件的第一个AddressHeader。
再回到EndpointAddress的定义,可以看到Header属性的类型就是AddressHeaderCollection,不过这仅仅是一个只读的属性,我们不能通过该属性为EndpointAddress添加AddressHeader。接下来介绍如何添加AddressHeader对象。
public class EndpointAddress { //其他成员 public Uri Uri { get; } public AddressHeaderCollection Headers { get; } public EndpointIdentity Identity { get; } }
如何为服务指定AddressHeader
在对服务寄宿的时候,可以通过代码和配置的方式为EndpointAddress添加相应的AddressHeader。下面的代码片断演示了在对服务进行寄宿的过程中如何设定AddressHeader。
using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService))) { string headerValue = "Licensed User"; string headerName = "UserType"; string headerNamespace = "http://www.artech.com/"; AddressHeader addressHeader = AddressHeader.CreateAddressHeader (headerName,headerNamespace,headerValue); EndpointAddress endpointAddress = new EndpointAddress(new Uri ("http://127.0.0.1:9999/CalculatorService"),addressHeader); ServiceEndpoint serviceEndpoint = new ServiceEndpoint (ContractDescription.GetContract(typeof(ICalculator)), new BasicHttpBinding(),endpointAddress); serviceHost.Description.Endpoints.Add(serviceEndpoint); serviceHost.Open(); //其他代码 }
上面的事例代码中,为了确定访问者的类型:购买了服务的用户(Licensed user)和免费试用的用户(Trivial user),添加了一个名称为UserType的AddressHeader,命名空间为http://www.artech.com/。我们希望该终结点只能让第一类用户进行访问,将AddressHeader的值设为Licensed User。先通过AddressHeader的静态方法CreateAddressHeader创建了一个AddressHeader对象,然后传入EndpointAddress的构造函数创建EndpointAddress。最后基于此EndpointAddress创建终结点对象,并添加到服务的终结点列表中。
在一般情况下,我们倾向于通过配置的方式来指定终结点的AddressHeader列表。AddressHeader定义在终结点<headers>配置项中,直接以XML的方式指定名称、命名空间和值。下面的一段配置和上面的代码是等效的。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="Artech.WcfServices.Services.CalculatorService"> <endpoint address="http://127.0.0.1:9999/CalculatorService" binding="basicHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator"> <headers> <UserType xmlns="http://www.artech.com/">Licensed User</UserType> </headers> </endpoint> </service> </services> </system.serviceModel> </configuration>
如何在客户端指定AddressHeader
对于客户端来说,同样具有两种不同的方式对AddressHeader进行设定:代码的方式和配置的方式。和在服务端的配置相似,客户端终结点的AddressHeader列表也定义在终结点的<headers>配置项中。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <client> <endpoint name="CalculatorService" address= "http://127.0.0.1:8888/CalculatorService" binding= "basicHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" > <headers> <UserType xmlns="urn:artech.com">Licensed User</UserType> </headers> </endpoint> </client> </system.serviceModel> </configuration>
如果采用配置的方式添加了上述AddressHeader,当我们采用该AddressHeader所在的终结点进行服务调用时,相应的MessageHeader将会自动附加到请求消息中,代码如下所示。
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <UserType xmlns="http://www.artech.com/">Licensed User</UserType> </s:Header> <s:Body> ... ... </s:Body> </s:Envelope>
如果不希望每次服务访问的消息都是自动附加上这么一段SOAP报头(可能该报头仅仅在某些确定的服务调用下才需要),在这种情况下,我们可以通过当前OperationContext的OutgoingMessageHeaders属性(OperationContext.Current.OutgoingMessageHeaders),直接将AddressHeader添加到出栈消息报头列表中,代码如下所示。
using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("CalculatorService")) { ICalculator calculator = channelFactory.CreateChannel(); using (calculator as IDisposable) { using (OperationContextScope contextScope = new OperationContextScope(calculator as IContextChannel)) { string headerValue = "Licensed User"; string headerName = "UserType"; string headerNamespace = "http://www.artech.com"; AddressHeader addressHeader = AddressHeader.CreateAddressHeader (headerName, headerNamespace, headerValue); OperationContext.Current.OutgoingMessageHeaders.Add( addressHeader.ToMessageHeader()); Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1,2, calculator.Add(1,2)); } } }
由于最终的实现是将MessageHeader添加到OutgoingMessageHeaders中,所以也可以绕开AddressHeader,直接添加一个与之等效的MessageHeader。
using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("CalculatorService")) { ICalculator calculator = channelFactory.CreateChannel(); using (calculator as IDisposable) { using (OperationContextScope contextScope = new OperationContextScope(calculator as IContextChannel)) { string headerValue = "Licensed User"; string headerName = "UserType"; string headerNamespace = "http://www.artech.com";
MessageHeader<string> messageHeader = new MessageHeader<string>(headerValue); OperationContext.Current.OutgoingMessageHeaders.Add (messageHeader.GetUntypedHeader(headerName,headerNamespace)); Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1,2, calculator.Add(1,2)); } } }
上面采用ChannelFactory<T>的方式编写了客户端的代码并进行了服务调用。当我们使用继承自ClientBase<T>的服务代理进行服务调用时,AddressHeader的指定方式具有小小的差异。
using (CalculatorClient CalculatorClient = new CalculatorClient("CalculatorService")) { using (OperationContextScope contextScope = new OperationContextScope (CalculatorClient.InnerChannel as IContextChannel)) { string headerValue = "Licensed User"; string headerName = "UserType"; string headerNamespace = "http://www.artech.com "; AddressHeader addressHeader = AddressHeader.CreateAddressHeader (headerName, headerNamespace, headerValue); OperationContext.Current.OutgoingMessageHeaders.Add (addressHeader.ToMessageHeader()); Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, CalculatorClient.Add(1, 2)); } }
由于在服务端我们为服务的终结点指定了AddressHeader,就意味着该终结点只接受消息的报头和与此AddressHeader相匹配的消息请求。我们可以通过实验证实这一点:通过下面的配置,我们将客户端AddressHeader的值从服务端希望的Licensed User变成Trivial User。由于服务端找不到相匹配的终结点导致如图2-3所示的EndpointNotFoundException异常被抛出。
图2-3 基于AddressHeader的消息筛选导致EndpointNotFoundException异常
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <client> <endpoint name="CalculatorService" address= "http://127.0.0.1:8888/CalculatorService" binding= "basicHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" > <headers> <UserType xmlns="urn:artech.com">Trivial User </UserType> </headers> </endpoint> </client> </system.serviceModel> </configuration>
本例实际上涉及的是消息筛选(Message Filter)问题,消息筛选解决的是如何通过接收的消息选择相应终结点的问题。其中地址筛选(Address Filter)是消息筛选的一种类型,WCF默认采用完全地址匹配的模式:地址的URI和AddressHeader均完全匹配。为了让服务端的终结点在AddressHeader和消息的报头不匹配的情况下也能处理消息请求,我们可以通过ServiceBeahviorAttribute改变地址筛选模式(AddressFilterMode)。比如在下面的代码中,通过ServiceBehaviorAttribute将AddressFilterMode设为AddressFilterMode.Any,从而避免了如图2-3所示的EndpointNotFoundException异常的抛出。关于消息筛选,在本章2.4节中还会详细介绍。
namespace Artech.WcfServices.Services { [ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)] public class CalculatorService:ICalculator { #region ICalculator Members public double Add(double x, double y) { return x + y; } #endregion } }