WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于WS-MEX的实现](提供模拟程序)

通过《如何将一个服务发布成WSDL[编程篇]》的介绍我们知道了如何可以通过编程或者配置的方式将ServiceMetadataBehavior这样一个服务形式应用到相应的服务上面,从而实现基于HTTP-GET或者WS-MEX的元数据发布机制。那么在WCF内部具体的实现原理又是怎样的呢?相信很多人对此都心存好奇,本篇文章的内容将围绕着这个主题展开。

一、 从WCF分发体系谈起

如果读者想对WCF内部的元数据发布机制的实现原理有一个全面而深入的了解,必须对WCF服务端的分发体系有一个清晰的认识。在这里我们先对该分发体系作一个概括性的介绍。WCF整个分发体系在进行服务寄宿(Hosting)时被构建,该体系的基本结构基本上可以通过图1体现。

image 图1 WCF服务端分发体系

当我们创建ServiceHost对象成功寄宿某个服务后,WCF会根据监听地址的不同为该ServiceHost对象创建一到多个ChannelDispatcher对象。每个ChannelDispatcher都拥有各自的ChannelListener,这些ChannelListener绑定到相应的监听地址监听来自外界的请求。对于每一个ChannelListener对象,有个自己具有一到多个EndpointDispatcher对象与之匹配,每一个EndpointDispatcher对应着某个终结点。

而针对每一个EndpointDispatcher,在其初始化的时候会为之创建一个运行时,即DispatchRuntimeDispatchRuntime拥有一系列处理请求、激活对象和执行方法等操作的运行时对象,在这里我们主要关注一个称为InstanceContextProvider的对象。InstanceContextProvider用于提供封装有相应服务实例的InstanceContext对象。

二、基于WS-MEX模式下的元数据发布是如何实现的?

现在我们再把话题移到元数据发布上来,先来谈谈基于WS-MEX协议的元数据发布方式。在这种元数据发布模式下,服务端通过MEX终结点发布元数据,客户端创建相应的MEX终结点获取元数据,这和一般意义上的服务调用并没有本质的不同。你完全可以将元数据的获取当成是一个某个服务,而该服务就是提供元数据。

如果我们通过编程或者配置的方式为某个服务添加了一个MEX终结点后,当服务被成功寄宿后,WCF会为之创建一个ChannelDispatcher。该ChannelDispatcher拥有一个用于监听元数据请求的ChannelListener,监听的地址及元数据发布的地址。基于该MEX终结点的EndpointDispatcher对象也会被创建,并与该ChannelDispatcher关联在一起。在EndpointDispatcher初始化的时候,关联DispatchRuntime也随之被创建。与普通终结点关联的DispatchRuntime一样,基于MEX终结点的DispatchRuntime同样拥有相同的运行时对象集合。但是,由于并没有一个真正用于提供元数据的服务被寄宿,DispatchRuntime的InstanceContextProvider(默认是PerSessionInstanceContextProvider)是获取不到包含有真正服务实例的InstanceContext对象的。

那么,如果能够定制DispatchRuntime的InstanceContextProvider,使它能够正常提供一个InstanceContext,而该InstanceContext包含真正能够提供元数据的服务实例,并且服务类类实现MEX终结点的契约接口IMetadataExchange,那么一切问题都迎刃而解。实际上,ServiceMetadataBehavior内部就是这么做的,而这个用于提供元数据的服务类型是定义在WCF内部的一个internal类型:WSMexImpl。

   1: internal class WSMexImpl : IMetadataExchange
   2: {
   3:     //其他成员  
   4:     public IAsyncResult BeginGet(Message request, AsyncCallback callback, object state);
   5:     public Message EndGet(IAsyncResult result);
   6:     private MetadataSet GatherMetadata(string dialect, string identifier);
   7:     public Message Get(Message request);
   8: }

ServiceMetadataBehaviorApplyDispatchBehavior方法被执行的时候,ServiceMetadataBehavior会创建WSMexImpl对象,据此创建InstanceContext对象,并将其作为MEX终结点DispatchRuntime的SingletonInstanceContext。然后创建一个SingletonInstanceContextProvider作为该DispatchRuntime的InstanceContextProvider。那么,MEX终结点的DispatchRuntime就能使用其InstanceContextProvider提供封装有WSMexImpl实例的InstanceContext了。

上诉的这些内容虽然不算负责,但是要求读者对WCF的实例上下文机制有清晰的认识,对此不太熟悉的读者,可以参数《WCF技术剖析(卷1)》第9章。为了加深读者对基于WS-MEX元数据发布机制的理解,接下来我会作一个简单的实例演示。

三、 实例演示:模拟ServiceMetadataBehavior实现基于WS-MEX元数据发布

接下来,我会完全基于ServiceMetadataBehavior的实现原理,即在上面介绍的原理,创建一个自定义服务行为用于基于WS-MEX的元数据发布,Source Code从这里下载。首先我们先来编写一些辅助性质的代码。由于在本例中我需要创建一些与DispatchRuntime相关的运行时对象,而且很多对象并没有被公开出来(很多是internal类型,比如SingletonInstanceContextProvider),我需要通过反射的机制来创建它们。此外,我们需要为某些对象的一些私有或者内部属性赋值,同样需要利用反射,所以我写了下面两个辅助方法:

   1: using System;
   2: using System.Globalization;
   3: using System.Reflection;
   4: namespace ServiceMetadataBehaviorSimulator
   5: {
   6:    public static class Utility
   7:     {
   8:        public static T CreateInstance<T>(string typeQname, Type[]parameterTypes, object[] parameters) where T:class
   9:        {
  10:            Type type = Type.GetType(typeQname);
  11:            BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
  12:            ConstructorInfo constructorInfo = type.GetConstructor(bindingFlags, Type.DefaultBinder, parameterTypes, null);
  13:            return Activator.CreateInstance(type, bindingFlags, Type.DefaultBinder, parameters, CultureInfo.InvariantCulture) as T;     
  14:        }
  15:  
  16:        public static void SetPropertyValue(object target, string propertyName, object propertyValue)
  17:        {
  18:            BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance;
  19:            PropertyInfo propertyInfo = target.GetType().GetProperty(propertyName, bindingFlags);
  20:            propertyInfo.SetValue(target, propertyValue, null);
  21:        }
  22:     }
  23: }

接下来,我仿照IMetadataExchange接口定义了如下一个接口:IMetadataProvisionService。为了简化,我省略了异步模式定义的Get操作(BeginGet/EndGet)。Get操作的Action和ReplyAction同样基于WS-Transfer规范定义。通过ServiceContractAttribute特性将契约的Name和ConfigurationName设定成IMetadataProvisionService

   1: using System.ServiceModel;
   2: using System.ServiceModel.Channels;
   3: namespace ServiceMetadataBehaviorSimulator
   4: {
   5:     [ServiceContract(ConfigurationName = "IMetadataProvisionService", Name = "IMetadataProvisionService", Namespace = "http://schemas.microsoft.com/2006/04/mex")]
   6:     public interface IMetadataProvisionService
   7:     {       
   8:         [OperationContract(Action = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get", ReplyAction = "http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse")]
   9:         Message Get(Message request);
  10:     }
  11: }

由于Get操作返回的是封装有元数据(以MetadataSet的形式)的消息对象,为此我定义了如下一个以消息契约(Message Contract)形式定义的类型:MetadataMessage。MetadataMessage通过MessageBodyMemberAttribute特性直接将类型为MetadataSet的属性定义成消息主体成员,并按照WS-MEX规范设置该成员的名称和命名空间。

   1: using System.ServiceModel;
   2: using System.ServiceModel.Description;
   3: namespace ServiceMetadataBehaviorSimulator
   4: {
   5:     [MessageContract(IsWrapped = false)]
   6:     public class MetadataMessage
   7:     {
   8:         public MetadataMessage(MetadataSet metadata)
   9:         {
  10:             this.Metadata = metadata;
  11:         }
  12:  
  13:         [MessageBodyMember(Name = "Metadata", Namespace = "http://schemas.xmlsoap.org/ws/2004/09/mex")]
  14:         public MetadataSet Metadata { get; set; }
  15:     }
  16: }

接下来我们来创建真正用于提供元数据的服务类:MetadataProvisionService。MetadataProvisionService实现了上面定义的服务契约接口IMetadataProvisionService,具有一个MetadataSet类型的属性成员Metadata。在Get方法中,通过Metadata属性表述的MetadataSet创建MetadataMessage对象,并将其转化成Message对象返回。最终返回的消息具有WS-Transfer规定的Action:http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse

   1: using System;
   2: using System.ServiceModel.Channels;
   3: using System.ServiceModel.Description;
   4: namespace ServiceMetadataBehaviorSimulator
   5: {
   6:     public class MetadataProvisionService : IMetadataProvisionService, 
   7:     {
   8:         public MetadataSet Metadata
   9:         { get; private set; }
  10:  
  11:         public MetadataProvisionService(MetadataSet metadata)
  12:         {
  13:             if (null == metadata)
  14:             {
  15:                 throw new ArgumentNullException("metadata");
  16:             }
  17:             this.Metadata = metadata;
  18:         }        
  19:  
  20:         public Message Get(System.ServiceModel.Channels.Message request)
  21:         {
  22:             MetadataMessage message = new MetadataMessage(this.Metadata);
  23:             TypedMessageConverter converter = TypedMessageConverter.Create(typeof(MetadataMessage), "http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse");
  24:             return converter.ToMessage(message, request.Version);
  25:         }
  26:     }
  27: }

最后我们就可以创建用于实现元数据发布的服务行为了,在这里使用了与ServiceMetadataBehavior相同的名字,并将其定义成特性,那么我们就可以直接通过特性的方式应用到服务类型上。所有的实现体现在ApplyDispatchBehavior方法中,该方法先后执行以下两组操作:

  • 导出元数据:直接通过WsdlExporter将服务相关的所有终结点导出生成MetadataSet,需要注意的是,在进行终结点收集的时候,需要过滤到MEX终结点;元数据导出的所有操作实现在GetExportedMetadata方法中;
  • 定制MEX终结点的DispatchRuntimeServiceHostEndpointDispatcher列表中,筛选出基于MEX终结点的EndpointDispatcher,然后定制它们的DispatchRuntime:创建SingletonInstanceContextProvider作为IntanceContextProvider;根据导出的MetadataSet创建MetadataProvisionService对象,并将其封装在InstanceContext中,而该InstanceContext直接设定为DispatchRuntime的SingletonInstanceContext。
   1: using System;
   2: using System.Collections.ObjectModel;
   3: using System.Reflection;
   4: using System.ServiceModel;
   5: using System.ServiceModel.Channels;
   6: using System.ServiceModel.Description;
   7: using System.ServiceModel.Dispatcher;
   8: using System.Xml;
   9: namespace ServiceMetadataBehaviorSimulator
  10: {
  11:     [AttributeUsage(AttributeTargets.Class)]
  12:     public class ServiceMetadataBehaviorAttribute:Attribute, IServiceBehavior
  13:     {
  14:         private const string MexContractName = "IMetadataProvisionService";
  15:         private const string MexContractNamespace = "http://schemas.microsoft.com/2006/04/mex";
  16:         private const string SingletonInstanceContextProviderType = "System.ServiceModel.Dispatcher.SingletonInstanceContextProvider,System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
  17:         public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters){}
  18:  
  19:         public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  20:         {
  21:             MetadataSet metadata = GetExportedMetadata(serviceDescription);
  22:             CustomizeMexEndpoints(serviceDescription, serviceHostBase, metadata);           
  23:         }
  24:  
  25:         private static MetadataSet GetExportedMetadata(ServiceDescription serviceDescription)
  26:         {
  27:             Collection<ServiceEndpoint> endpoints = new Collection<ServiceEndpoint>();
  28:             foreach (var endpoint in serviceDescription.Endpoints)
  29:             {
  30:                 if (endpoint.Contract.ContractType == typeof(IMetadataProvisionService))
  31:                 {
  32:                     continue;
  33:                 }
  34:                 ServiceEndpoint newEndpoint = new ServiceEndpoint(endpoint.Contract, endpoint.Binding, endpoint.Address);
  35:                 newEndpoint.Name = endpoint.Name;
  36:                 foreach (var behavior in endpoint.Behaviors)
  37:                 {
  38:                     newEndpoint.Behaviors.Add(behavior);
  39:                 }
  40:                 endpoints.Add(newEndpoint);
  41:             }
  42:             WsdlExporter exporter = new WsdlExporter();
  43:             XmlQualifiedName wsdlServiceQName = new XmlQualifiedName(serviceDescription.Name, serviceDescription.Namespace);
  44:             exporter.ExportEndpoints(endpoints, wsdlServiceQName);
  45:             MetadataSet metadata = exporter.GetGeneratedMetadata();
  46:             return metadata; 
  47:         }
  48:  
  49:         private static void CustomizeMexEndpoints(ServiceDescription description, ServiceHostBase host,MetadataSet metadata)
  50:         {
  51:             foreach(ChannelDispatcher channelDispatcher in host.ChannelDispatchers)
  52:             {
  53:                 foreach(EndpointDispatcher endpoint in channelDispatcher.Endpoints)
  54:                 {
  55:                     if (endpoint.ContractName == MexContractName && endpoint.ContractNamespace == MexContractNamespace)
  56:                     {
  57:                         DispatchRuntime dispatchRuntime = endpoint.DispatchRuntime;
  58:                         dispatchRuntime.InstanceContextProvider = Utility.CreateInstance<IInstanceContextProvider>(SingletonInstanceContextProviderType, new Type[] { typeof(DispatchRuntime) }, new object[] { dispatchRuntime });
  59:                         MetadataProvisionService serviceInstance = new MetadataProvisionService(metadata);
  60:                         dispatchRuntime.SingletonInstanceContext = new InstanceContext(host, serviceInstance);
  61:                     }
  62:                 }
  63:             } 
  64:         }
  65:  
  66:         public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }       
  67:     }
  68: }

以我们熟悉的计算服务的为例,我们将ServiceMetadataBehaviorAttribute直接应用CalculatorService上。下面是CalculatorService的定义,之所以让它实现我们定义的IMetadataProvisionService接口,是为了在进行服务寄宿是满足服务类型比如实现终结点契约接口的约束。如果直接使用WCF提供IMetadataExchange,由于其内部进行了相应的处理,服务类型与MEX终结点契约接口无关时允许的。

   1: using System;
   2: using System.ServiceModel.Channels;
   3: using System.ServiceModel.Description;
   4: using Artech.Contracts;
   5: using ServiceMetadataBehaviorSimulator;
   6: namespace Artech.Services
   7: {
   8:     [ServiceMetadataBehavior()]
   9:     public class CalculatorService : ICalculator, IMetadataProvisionService
  10:     {
  11:         public double Add(double x, double y)
  12:         {
  13:             return x + y;
  14:         }
  15:  
  16:         public Message Get(Message request)
  17:         {
  18:             throw new NotImplementedException();
  19:         }
  20:     }
  21: }

那么在进行服务寄宿的时候,我们就可以采用WCF如下的方式添加MEX终结点了。可以看到这与WCF本身支持的MEX终结点的配置一本上是一样的,唯一不同的是这里的契约是我们自定义IMetadataProvisionService,而不是IMetadataExchange。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>   
   3:     <system.serviceModel>
   4:         <services>
   5:             <service name="Artech.Services.CalculatorService">
   6:                 <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="ws2007HttpBinding" contract="Artech.Contracts.ICalculator" />
   7:                 <endpoint address="http://127.0.0.1:9999/calculatorservice/mex"
   8:                     binding="mexHttpBinding" contract="IMetadataProvisionService" />
   9:             </service>
  10:         </services>
  11:     </system.serviceModel>
  12: </configuration>

在客户端就可以采用与一般服务调用完全一样的方式获取服务的元数据了,下面是客户端的配置。注意,这里配置的终结点并不是调用Calculatorservice的终结点,而是为了获取元数据的MEX终结点。地址是服务端MEX终结点的地址,契约是IMetadataProvisionService,采用的绑定是标准的基于HTTP的MEX绑定

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>       
   4:         <client>
   5:             <endpoint address="http://127.0.0.1:9999/calculatorservice/mex"
   6:                 binding="mexHttpBinding" contract="IMetadataProvisionService"
   7:                 name="mex" />
   8:         </client>
   9:     </system.serviceModel>
  10: </configuration>

下面是基于ChannelFactory<TChannel>创建服务代理的客户端代码,可以看到与一般的服务调用并无二致。获取的元数据最终被写入一个XML文件并被打开。

   1: using System.Diagnostics;
   2: using System.ServiceModel;
   3: using System.ServiceModel.Channels;
   4: using System.ServiceModel.Description;
   5: using System.Text;
   6: using System.Xml;
   7: using ServiceMetadataBehaviorSimulator;
   8: namespace Artech.Client
   9: {
  10:     class Program
  11:     {
  12:         static void Main(string[] args)
  13:         {
  14:             using (ChannelFactory<IMetadataProvisionService> channelFactory = new ChannelFactory<IMetadataProvisionService>("mex"))
  15:             {
  16:                 IMetadataProvisionService proxy = channelFactory.CreateChannel();
  17:                 Message request = Message.CreateMessage(MessageVersion.Default, "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get");
  18:                 Message reply = proxy.Get(request);
  19:                 MetadataSet metadata = reply.GetBody<MetadataSet>();
  20:                 using (XmlWriter writer = new XmlTextWriter("metadata.xml", Encoding.UTF8))
  21:                 {
  22:                     metadata.WriteTo(writer);
  23:                 }
  24:                 Process.Start("metadata.xml");
  25:             }
  26:         }
  27:     }
  28: }

上面的应用如果正常执行,包含所有元数据信息的XML文件将会通过IE(假设使用IE作为开启XML文件的默认应用程序)开启,图2是运行后的截图:

image 图2 获取的元数据在IE中的显示

下一篇中我们将采用同样的方式来模拟基于HTTP-GET的元数据发布时如何实现的。

作者:Artech
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2009-12-26 18:35 Artech 阅读(...) 评论(...) 编辑 收藏