有时候,客户端需要通过编程方式验证一个特定的终结点(通过地址进行识别)是否支持一个特定的契约。设想有这样一个应用程序,终端用户在安装时(甚至在运行时)指定或配置应用程序,用以使用服务并与服务交互。如果服务不支持所需的契约,应用程序就会向用户发出警告,提示配置的地址是无效的,询问是否更正地址或替换地址。例如,第10章使用的证书管理器应用程序(Credentials Manager Application)就具备这样的特征:用户需要为应用程序提供管理账户成员与角色的安全证书服务的地址。在验证了地址支持所需的服务契约之后,证书管理器只允许用户选择有效的地址。
编程处理元数据
为了支持这一功能,应用程序需要获取服务终结点的元数据,查看是否存在至少一个终结点支持请求的契约。正如第1章阐释的那样,如果元数据交换终结点是服务支持的,或者基于HTTP-GET协议,那么元数据在这个终结点中就是可用的。当我们使用HTTP-GET协议时,元数据交换的地址就是HTTP-GET地址(通常,服务的基地址以?wsdl为后缀)。为了简化对返回元数据的解析工作,WCF提供了几个辅助类,位于System.ServiceModel.Description命名空间,如例2-7所示。
例2-7:支持元数据处理的类型
public enum MetadataExchangeClientMode
{
MetadataExchange,
HttpGet
}
class MetadataSet : ...
{...}
public class ServiceEndpointCollection : Collection<ServiceEndpoint>
{...}
public class MetadataExchangeClient
{
public MetadataExchangeClient();
public MetadataExchangeClient(Binding mexBinding);
public MetadataSet GetMetadata(Uri address,MetadataExchangeClientMode mode);
//更多成员
}
public abstract class MetadataImporter
{
public abstract ServiceEndpointCollection ImportAllEndpoints();
//更多成员
}
public class WsdlImporter : MetadataImporter
{
public WsdlImporter(MetadataSet metadata);
//更多成员
}
public class ServiceEndpoint
{
public EndpointAddress Address
{get;set;}
public Binding Binding
{get;set;}
public ContractDescription Contract
{get;}
//更多成员
}
public class ContractDescription
{
public string Name
{get;set;}
public string Namespace
{get;set;}
//更多成员
}
MetadataExchangeClient能够使用与元数据交换关联的绑定,该元数据交换保存在应用程序的配置文件中。我们也可以将初始化后的绑定实例传递给MetadataExchange-Client的构造函数。传递的绑定实例包含一些自定义值,例如容量。如果返回的元数据超过默认的接收消息大小时,为了接收更大的消息,就可以设置容量值。MetadataExchangeClient的GetMetadata()方法接收一个终结点地址实例,它封装了元数据交换地址以及一个枚举值,指定了访问的方式。方法返回的元数据放在一个MetadataSet实例中。我们不需要直接操作MetadataSet类型,而是创建MetadataImporter类的子类实例,例如WsdlImporter,将原来的元数据传递给它的构造函数,然后调用ImportAllEndpoints()方法,获取在元数据中查找到的所有终结点的集合。终结点以ServiceEndpoint类型方式表示。
ServiceEndpoint定义了ContractDescription类型属性Contract。Contract-Description类定义了契约的名称与命名空间。
使用HTTP-GET时,为了判断配置的基地址是否支持特定的契约,通过刚才描述的步骤就能够生成终结点的集合。遍历集合的每一个终结点,比较请求契约中Contract-Description的Name和Namespace属性值,如例2-8所示。
例2-8:查询契约的地址
bool contractSupported = false;
string mexAddress = "...?WSDL";
MetadataExchangeClient MEXClient = new MetadataExchangeClient(new Uri(mexAddress),
MetadataExchangeClientMode.HttpGet);
MetadataSet metadata = MEXClient.GetMetadata();
MetadataImporter importer = new WsdlImporter(metadata);
ServiceEndpointCollection endpoints = importer.ImportAllEndpoints();
foreach(ServiceEndpoint endpoint in endpoints)
{
if(endpoint.Contract.Namespace == "MyNamespace" &&
endpoint.Contract.Name == "IMyContract")
{
contractSupported = true;
break;
}
}
注意:第1章提到的元数据浏览器工具采用的步骤与例2-8获取服务终结点的步骤相似。如果给定一个基于HTTP的地址,工具会同时尝试使用HTTP-GET和基于HTTP的元数据交换终结点。元数据浏览器也能够使用基于TCP或IPC的元数据交换终结点获取元数据。工具的大量实现都是用于处理元数据,以及显示元数据的内容,毕竟,WCF提供的类很难获取和解析元数据。
MetadataHelper类
我们将例2-8所示的步骤封装到了设计的通用静态工具类MetadataHelper()的QueryContract()方法中:
public static class MetadataHelper
{
public static bool QueryContract(string mexAddress,Type contractType);
public static bool QueryContract(string mexAddress,string contractNamespace,
string contractName);
//更多成员
}
可以为MetadataHelper类提供我们希望查询的契约类型,或者提供该契约的名称与命名空间:
string address = "...";
bool contractSupported = MetadataHelper.QueryContract(address,typeof(IMyContract));
至于QueryContract()方法中的元数据交换地址mexAddress,我们可以提供带HTTP-GET地址的MetadataHelper类,也可以提供基于HTTP、HTTPS、TCP或者IPC的元数据交换终结点地址。例2-9演示了MetadataHelper.QueryContract()方法的实现,它省略了错误处理的代码。
例2-9:MetadataHelper.QueryContract()的实现
public static class MetadataHelper
{
const int MessageMultiplier = 5;
static ServiceEndpointCollection QueryMexEndpoint(string mexAddress,
BindingElement bindingElement)
{
CustomBinding binding = new CustomBinding(bindingElement);
MetadataExchangeClient MEXClient = new MetadataExchangeClient(binding);
MetadataSet metadata = MEXClient.GetMetadata
(new EndpointAddress(mexAddress));
MetadataImporter importer = new WsdlImporter(metadata);
return importer.ImportAllEndpoints();
}
public static ServiceEndpoint[] GetEndpoints(string mexAddress)
{
/* 一些错误处理 */
Uri address = new Uri(mexAddress);
ServiceEndpointCollection endpoints = null;
if(address.Scheme == "net.tcp")
{
TcpTransportBindingElement tcpBindingElement =
new TcpTransportBindingElement();
tcpBindingElement.MaxReceivedMessageSize *= MessageMultiplier;
endpoints = QueryMexEndpoint(mexAddress,tcpBindingElement);
}
if(address.Scheme == "net.pipe")
{...}
if(address.Scheme == "http") //判断是否为HTTP-GET
{...}
if(address.Scheme == "https") //判断是否为HTTPS-GET
{...}
return Collection.ToArray(endpoints);
}
public static bool QueryContract(string mexAddress,Type contractType)
{
if(contractType.IsInterface == false)
{
Debug.Assert(false,contractType + " is not an interface");
return false;
}
object[] attributes = contractType.GetCustomAttributes(
typeof(ServiceContractAttribute),false);
if(attributes.Length == 0)
{
Debug.Assert(false,"Interface " + contractType +
" does not have the ServiceContractAttribute");
return false;
}
ServiceContractAttribute attribute = attributes[0] as
ServiceContractAttribute;
if(attribute.Name == null)
{
attribute.Name = contractType.ToString();
}
if(attribute.Namespace == null)
{
attribute.Namespace = "http://tempuri.org/";
}
return QueryContract(mexAddress,attribute.Namespace,attribute.Name);
}
public static bool QueryContract(string mexAddress,string contractNamespace,
string contractName)
{
if(String.IsNullOrEmpty(contractNamespace))
{
Debug.Assert(false,"Empty namespace");
return false;
}
if(String.IsNullOrEmpty(contractName))
{
Debug.Assert(false,"Empty name");
return false;
}
try
{
ServiceEndpoint[] endpoints = GetEndpoints(mexAddress);
foreach(ServiceEndpoint endpoint in endpoints)
{
if(endpoint.Contract.Namespace == contractNamespace &&
endpoint.Contract.Name == contractName)
{
return true;
}
}
}
catch
{}
return false;
}
}
在例2-9中,GetEndpoints()方法对元数据交换地址的样式进行了解析。根据找到的传输样式(例如TCP),GetEndpoints()方法创建了一个需要使用的绑定元素,这样就可以设置它的MaxReceivedMessageSize属性值:
public abstract class TransportBindingElement : BindingElement
{
public virtual long MaxReceivedMessageSize
{get;set;}
}
public abstract class ConnectionOrientedTransportBindingElement :
TransportBindingElement,...
{...}
public class TcpTransportBindingElement : ConnectionOrientedTransportBindingElement
{...}
MaxReceiveMessageSize的默认值为64K。它适用于简单的服务。如果服务包含多个终结点,终结点又使用了复杂类型,就会生成更大的消息。此时,调用MetadataExchangeClient.GetMetadata()方法就会失败。根据经验,大多数情况下最合适的倍数因子是5。接着,GetEndpoints()调用了QueryMexEndpoint()私有方法,以获取元数据。QueryMexEndpoint()接收元数据交换终结点的地址以及要使用的绑定元素。使用绑定元素是为了创建定制绑定,并将它提供给MetadataExchange-Client实例。MetadataExchangeClient实例能够获取元数据,返回终结点集合。但是,GetEndpoints()方法返回的终结点集合不是ServiceEndpoint-Collection类型,而是使用了我们设计的Collection辅助类,返回了一个终结点数组。
接收Type参数的QueryContract()方法首先会验证传入的Type类型是否是接口类型,如果是,则判断该接口是否标记了ServiceContract特性。因为ServiceContract特性可以为契约的请求类型指定名称和命名空间的别名,QueryContract()使用这些值查询符合条件的契约。如果没有指定别名,QueryContract()方法则使用类型的名字与默认的命名空间http://tempuri.org,然后调用另一个重载版本的QueryContract()方法,它能够操作契约的名称和命名空间。该版本的QueryContract()方法调用了GetEndpoints()方法,以获得终结点数组,然后遍历该数组。如果找到至少一个终结点支持该契约,则返回true。不管出现何种错误,QueryContract()方法都会返回false。
例2-10介绍了MetadataHelper类定义的额外的查询元数据的方法。
例2-10:MetadataHelper类
public static class MetadataHelper
{
public static ServiceEndpoint[] GetEndpoints(string mexAddress);
public static string[] GetAddresses(Type bindingType,string mexAddress,
Type contractType);
public static string[] GetAddresses(string mexAddress,Type contractType);
public static string[] GetAddresses(Type bindingType,string mexAddress,
string contractNamespace,string contractName)
where B: Binding;
public static string[] GetAddresses(string mexAddress,string contractNamespace,
string contractName);
public static string[] GetContracts(Type bindingType,string mexAddress);
public static string[] GetContracts(string mexAddress);
public static string[] GetOperations(string mexAddress,Type contractType);
public static string[] GetOperations(string mexAddress,
string contractNamespace,
string contractName);
public static bool QueryContract(string mexAddress,Type contractType);
public static bool QueryContract(string mexAddress,
string contractNamespace,string contractName);
//更多成员
}
在管理程序和管理工具中,或者在对契约进行设置时,往往需要这些强大而有效的功能。它们的实现都是基于对终结点数组的处理,终结点数组则是通过GetEndpoints()方法返回的。
GetAddresses()方法返回的终结点地址,要么支持一个特定的契约,要么就是使用特定绑定的终结点地址。
相似的,GetContracts()方法返回的所有契约,要么被所有终结点支持,要么就是使用了特定绑定的所有终结点支持的契约。最后,GetOperations()方法会返回一个特定契约的所有操作。
注意:第10章的证书管理器应用程序使用了MetadataHelper类,附录B则使用它来管理持久订阅者。





