元数据交换
服务有两种方案可以发布自己的元数据。一种是基于HTTP-GET协议提供元数据,另一种则是后面将要讨论的使用专门的终结点的方式。WCF能够为服务自动提供基于HTTP-GET的元数据,但需要显式地添加服务行为(Behavior)以支持这一功能。本书后面的章节会介绍行为的相关知识。现在,我们只需要知道行为属于服务的本地特性,例如是否需要基于HTTP-GET交换元数据,就是一种服务行为。我们可以通过编程方式或管理方式添加行为。在例1-10演示的宿主应用程序的配置文件中,所有引用了定制<behavior>配置节的托管服务都支持基于HTTP-GET协议实现元数据交换。为了使用HTTP-GET,客户端使用的地址需要注册服务的HTTP基地址。我们也可以在行为中指定一个外部URL以达到同样的目的。
例1-10:使用配制文件启用元数据交换行为
<system.serviceModel>
<services>
<service name = "MyService" behaviorConfiguration = "MEXGET">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8000/"/>
</baseAddresses>
</host>
...
</service>
<service name = "MyOtherService" behaviorConfiguration = "MEXGET">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8001/"/>
</baseAddresses>
</host>
...
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name = "MEXGET">
<serviceMetadata httpGetEnabled = "true"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
一旦启用了基于HTTP-GET的元数据交换,在浏览器中就可以通过HTTP基地址(如果存在)进行访问。如果一切正确,就会获得一个确认页面,如图1-6所示,告知开发者已经成功托管了服务。确认页面与IIS托管无关,即使使用自托管,我们也可以使用浏览器定位服务地址。

图1-6:服务的确认页面
编程方式启用元数据交换
若要以编程方式启用基于HTTP-GET的元数据交换,首先需要将行为添加到行为集合中,该行为集合是宿主针对服务类型而维持的。ServiceHostBase类定义了Description属性,类型为ServiceDescription:
public abstract class ServiceHostBase : ...
{
public ServiceDescription Description
{get;}
//更多成员
}
顾名思义,ServiceDescription就是对服务各个方面与行为的描述。Service-Description类定义了类型为KeyedByTypeCollection<I>的属性Behaviors。其中,类型KeyedByTypeCollection<I>的泛型参数类型为IServiceBehavior接口:
public class KeyedByTypeCollection<I> : KeyedCollection<Type,I>
{
public T Find<T>();
public T Remove<T>();
//更多成员
}
public class ServiceDescription
{
public KeyedByTypeCollection<IServiceBehavior> Behaviors
{get;}
}
所有行为的类与特性均实现了IServiceBehavior接口。KeyedByTypeCollection<I>定义了泛型方法Find<T>(),它能够返回包含在集合中的请求行为,如果在集合中没有找到,则返回null。查询集合时,最多只能有一个返回的符合条件的行为类型。例1-11演示了如何通过编程方式启用行为。
例1-11:编程方式启用元数据交换行为
ServiceHost host = new ServiceHost(typeof(MyService));
ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{
metadataBehavior = new ServiceMetadataBehavior();
metadataBehavior.HttpGetEnabled = true;
host.Description.Behaviors.Add(metadataBehavior);
}
host.Open();
首先,托管代码调用KeyedByTypeCollection<I>的Find<T>()方法,它负责判断配置文件是否提供MEX终结点行为。Find<T>方法的类型参数为ServiceMetadata-Behavior类型。ServiceMetadataBehavior类定义在System.ServiceModel. Description命名空间下:
public class ServiceMetadataBehavior : IServiceBehavior
{
public bool HttpGetEnabled
{get;set;}
//更多成员
}
如果返回的行为为null,托管代码就会创建一个新的ServiceMetadataBehavior对象,并将HttpGetEnabled属性值设为true,然后将它添加到服务描述的behaviors属性中。
元数据交换终结点
元数据交换终结点是一种特殊的终结点,有时候又被称为MEX终结点。服务能够根据元数据交换终结点发布自己的元数据。图1-7展示了一个具有业务终结点和元数据交换终结点的服务。不过,在通常情况下并不需要在设计图中显示元数据交换终结点。

图1-7:元数据交换终结点
元数据交换终结点支持元数据交换的行业标准,在WCF中表现为IMetadataExchange接口:
[ServiceContract(...)]
public interface IMetadataExchange
{
[OperationContract(...)]
Message Get(Message request);
//更多成员
}
IMetadataExchange接口定义的细节并不合理。它与多数行业标准相似,都存在难以实现的问题。所幸,WCF自动地为服务宿主提供了IMetadataExchange接口的实现,公开元数据交换终结点。我们只需要指定使用的地址和绑定,以及添加服务元数据行为。对于绑定,WCF提供了专门的基于HTTP、HTTPS、TCP和IPC协议的绑定传输元素。对于地址,我们可以提供完整的地址,或者使用任意一个注册了的基地址。没有必要启用HTTP-GET选项,但是即使启用了也不会造成影响。例1-12演示的服务公开了三个MEX终结点,分别基于HTTP、TCP和IPC。出于演示的目的,TCP和IPC的MEX终结点使用了相对地址,HTTP则使用了绝对地址。
例1-12:添加MEX终结点
<services>
<service name = "MyService" behaviorConfiguration = "MEX">
<host>
<baseAddresses>
<add baseAddress = "net.tcp://localhost:8001/"/>
<add baseAddress = "net.pipe://localhost/"/>
</baseAddresses>
</host>
<endpoint
address = "MEX"
binding = "mexTcpBinding"
contract = "IMetadataExchange"
/>
<endpoint
address = "MEX"
binding = "mexNamedPipeBinding"
contract = "IMetadataExchange"
/>
<endpoint
address = http://localhost:8000/MEX
binding = "mexHttpBinding"
contract = "IMetadataExchange"
/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name = "MEX">
<serviceMetadata/>
</behavior>
</serviceBehaviors>
</behaviors>
编程方式添加MEX终结点
与其他终结点相似,我们只能在打开宿主之前通过编码方式添加元数据交换终结点。WCF并没有为元数据交换终结点提供专门的绑定类型。为此,我们需要创建定制绑定。定制绑定使用了与之匹配的传输绑定元素,然后将绑定元素对象作为构造函数的参数,传递给定制绑定实例。最后,调用宿主的AddServiceEndpoint()方法,参数值分别为地址、定制绑定与IMetadataExchange契约类型。例1-13的代码添加了基于TCP的MEX终结点。注意,在添加终结点之前,必须校验元数据行为是否存在。
例1-13:编程方式添加TCP MEX终结点
BindingElement bindingElement = new TcpTransportBindingElement();
CustomBinding binding = new CustomBinding(bindingElement);
Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/");
ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress);
ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{
metadataBehavior = new ServiceMetadataBehavior();
host.Description.Behaviors.Add(metadataBehavior);
}
host.AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
host.Open();
简化ServiceHost<T>类
我们可以扩展ServiceHost<T>类,从而自动实现例1-11和例1-13中的代码。ServiceHost<T>定义了Boolean型属性EnableMetadataExchange,通过调用该属性添加HTTP-GET元数据行为和MEX终结点:
public class ServiceHost<T> : ServiceHost
{
public bool EnableMetadataExchange
{get;set;}
public bool HasMexEndpoint
{get;}
public void AddAllMexEndPoints();
//更多成员
}
如果EnableMetadataExchange属性设置为true,就会添加元数据交换行为。如果没有可用的MEX终结点,它就会为每个已注册的基地址样式添加一个MEX终结点。使用ServiceHost<T>,例1-11和例1-13就可以简化为:
ServiceHost<MyService> host = new ServiceHost<MyService>();
host.EnableMetadataExchange = true;
host.Open();
ServiceHost<T>还定义了Boolean属性HasMexEndpoint。如果服务包含了任意一个MEX终结点(与传输协议无关),则返回true。ServiceHost<T>定义的AddAllMexEndPoints()方法可以为每个已注册的基地址添加一个MEX终结点,这些基地址的样式类型包括HTTP、TCP或IPC。例1-14介绍了这些方法的实现。
例1-14:实现EnableMetadataExchange以及它支持的方法
public class ServiceHost<T> : ServiceHost
{
public bool EnableMetadataExchange
{
Set
{
if(State == CommunicationState.Opened)
{
throw new InvalidOperationException("Host is already opened");
}
ServiceMetadataBehavior metadataBehavior;
metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{
metadataBehavior = new ServiceMetadataBehavior();
metadataBehavior.HttpGetEnabled = value;
Description.Behaviors.Add(metadataBehavior);
}
if(value == true)
{
if(HasMexEndpoint == false)
{
AddAllMexEndPoints();
}
}
}
Get
{
ServiceMetadataBehavior metadataBehavior;
metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{
return false;
}
return metadataBehavior.HttpGetEnabled;
}
}
public bool HasMexEndpoint
{
Get
{
Predicate<ServiceEndpoint> mexEndPoint= delegate(ServiceEndpoint endpoint)
{
return endpoint.Contract.ContractType == typeof(IMetadataExchange);
};
return Collection.Exists(Description.Endpoints,mexEndPoint);
}
}
public void AddAllMexEndPoints()
{
Debug.Assert(HasMexEndpoint == false);
foreach(Uri baseAddress in BaseAddresses)
{
BindingElement bindingElement = null;
switch(baseAddress.Scheme)
{
case "net.tcp":
{
bindingElement = new TcpTransportBindingElement();
break;
}
case "net.pipe":
{...}
case "http":
{...}
case "https":
{...}
}
if(bindingElement != null)
{
Binding binding = new CustomBinding(bindingElement);
AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
}
}
}
}
EnableMetadataExchange通过判断CommunicationObject基类的State属性值,确保宿主没有被打开。如果在配置文件中没有找到元数据行为,EnableMetadataExchange不会重写配置文件中的配置值,而只是将value赋给新建的元数据行为对象metadata behavior的HttpGetEnabled属性。读取EnableMetadataExchange的值时,属性首先会检查值是否已经配置。如果没有配置元数据行为,则返回false;否则返回它的HttpGetEnabled值。HasMexEndpoint属性将匿名方法(注1)赋给Predicate泛型委托。匿名方法负责检查给定终结点的契约是否属于IMetadataExchange类型。然后,调用Collection静态类的Exists()方法,方法的参数值为服务宿主中可用的终结点集合。Exists()方法将遍历集合中的每个元素并调用Predicate泛型委托对象mexEndPoint,如果集合中的任意一个元素符合Predicate指定的比较条件(也就是说,如果匿名方法的调用返回true),则返回true,否则返回false。AddAllMexEnd-Points()方法会遍历BaseAddresses集合。根据基地址的样式,创建匹配的MEX传输绑定元素,然后再创建一个定制绑定,并将它传入到AddServiceEndpoint()中,就像例1-13那样添加终结点。
元数据浏览器
元数据交换终结点提供的元数据不仅描述了契约与操作,还包括关于数据契约、安全性、事务性、可靠性以及错误的信息。为了可视化表示正在运行的服务的元数据,我们开发了元数据浏览器工具,它的实现包含在本书附带的源代码中。图1-8显示了使用元数据浏览器获得的例1-7定义的终结点。若要使用元数据浏览器,只需要提供HTTP-GET地址或者正在运行的服务的元数据交换终结点,就能获取返回的元数据。

图1-8:元数据浏览器





