在《原理篇》中我们谈到了通过自定义ServiceHost对WCF进行扩展的本质,以及在IIS/WAS寄宿情况下ServiceHostFactory的作用。接下来通过一个具体的例子来演示如何通过WCF扩展实现以Unity为代表的IoC框架的集成,以及应用该扩展的ServiceHost和ServiceHostFactory如何定义。[源代码从这里下载]
目录
一、IoC/DI简介
步骤一、自定义InstanceProvider:UnityInstanceProvider
步骤二、创建服务行为:UnityServiceBehaviorAttribute
步骤三、自定义ServiceHost:UnityServiceHost
步骤四、自定义ServiceHostFactory:UnityServiceHostFactory
步骤五、创建实例程序应用自定义ServiceHost
一、IoC/DI简介
所谓控制反转(IoC: Inversion Of Control)就是应用本身不负责依赖对象的创建和维护,而交给一个外部容器来负责。这样控制权就由应用转移到了外部IoC容器,控制权就实现了所谓的反转。比如,在类型A中需要使用类型B的实例,而B实例的创建并不由A来负责,而是通过外部容器来创建。
有时我们又将IoC成为依赖注入(DI: Dependency Injection)。所谓依赖注入,就是由外部容器在运行时动态地将依赖的对象注入到组件之中。具体的依赖注入方式又包括如下三种典型的形式。
构造器注入(Constructor Injection):IoC容器会智能地选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前会自定义创建相应参数对象;
属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性;
方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法。
在开源社区,具有很有流行的IoC框架,比如Castle Windsor、Unity、Spring.NET、StructureMap、Ninject等。现在我们就以Unity为例,介绍通过WCF的扩展如何实现基于IoC的服务实例的创建。
步骤一、自定义InstanceProvider:UnityInstanceProvider
要实现WCF和Unity之间的集成,最终体现在如何通过Unity容器来创建服务实例。而从前面介绍的关于服务端运行时框架的介绍,我们知道最终服务实例的提供落在了一个特殊的组件之上,即InstanceProvider。所以,本实例的核心就是要自定义一个采用Unity实现服务实例提供机制的自定义InstanceProvider。我们将之命名为UnityInstanceProvider。
下面的代码给出了实现IInstanceProvider接口的UnityInstanceProvider的定义。在构造函数中指定连个参数:实现了IUnityContainer接口的Unity容器对象和服务契约类型。那么在真正实现对服务实例创建的GetInstance方法上,直接调用IUnityContainer的Resolve方法传入给定的服务契约类型来创建具体的人服务实例。而在ReleaseInstance方法中则直接调用IUnityContainer的Teardown方法进行服务实例的释放。注意,在这之前需要确保如下两个基于Unity的程序集被引用:Microsoft.Practices.Unity.dll和Microsoft.Practices.Unity.Configuration.dll。
1: using System;
2: using System.ServiceModel;
3: using System.ServiceModel.Channels;
4: using System.ServiceModel.Dispatcher;
5: using Microsoft.Practices.Unity;
6: namespace Artech.WcfExtensions.IoC
7: {
8: public class UnityInstanceProvider: IInstanceProvider
9: {
10: public IUnityContainer UnityContainer { get; private set; }
11: public Type ContractType { get; private set; }
12:
13: public UnityInstanceProvider(IUnityContainer unityContainer, Type contractType)
14: {
15: this.UnityContainer = unityContainer;
16: this.ContractType = contractType;
17: }
18:
19: public object GetInstance(InstanceContext instanceContext, Message message)
20: {
21: return this.UnityContainer.Resolve(this.ContractType);
22: }
23:
24: public object GetInstance(InstanceContext instanceContext)
25: {
26: return this.GetInstance(instanceContext, null);
27: }
28:
29: public void ReleaseInstance(InstanceContext instanceContext, object instance)
30: {
31: this.UnityContainer.Teardown(instance);
32: }
33: }
34: }
步骤二、创建服务行为:UnityServiceBehaviorAttribute
和前面一个关于客户端语言文化上下文自动传播的实例一样,我们自定义的组件最终通过相应的行为应用到WCF的运行时中。为此,我们针对上面自定义的InstanceProvider定义了一个实现IServiceBehavior接口的服务行为UnityServiceBehaviorAttribute。为了能让该服务行为以声明的方式直接应用到服务类型上,我们将其定义成特性。
UnityServiceBehaviorAttribute的所有定义体现在如下所示的代码片断中。构造函数中具有一个字符串类型的参数containerName表示配置的Unity容器的名称。真正的容器名称在构造函数中被获取,为了避免UnityConainter的频繁创建,我们定义了一个静态的以容器名称为键值的字典保存已经被创建的Unity容器。
我们最终的目的是根据给定名称的Unity容器创建上面定义的UnityInstanceProvider,然后将其作为基于终结点的DispatchRuntime的InstanceProvider,这样的功能定义在ApplyDispatchBehavior方法中。创建UnityInstanceProvider还需要服务契约的类型,而得到服务契约类型采用了这样的逻辑:首先根据当前EndpointDispatcher得到契约名称和命名空间,然后通过ServiceHostBase得到表示服务描述的ServiceDescription对象,然后根据前面得到的契约名称和命名空间找到对应的表示契约描述的ContractDescription对象,而该对象的ContractType属性表示服务契约的类型。此外,如果基于契约类型的注册不存在,ApplyDispatchBehavior方法还进行了服务契约类型和服务类型之间的类型注册。
1: using System;
2: using System.Collections.Generic;
3: using System.Collections.ObjectModel;
4: using System.Configuration;
5: using System.Linq;
6: using System.ServiceModel;
7: using System.ServiceModel.Channels;
8: using System.ServiceModel.Description;
9: using System.ServiceModel.Dispatcher;
10: using Microsoft.Practices.Unity;
11: using Microsoft.Practices.Unity.Configuration;
12:
13: namespace Artech.WcfExtensions.IoC
14: {
15: public class UnityServiceBehaviorAttribute: Attribute, IServiceBehavior
16: {
17: static Dictionary<string, IUnityContainer> containers = new Dictionary<string, IUnityContainer>();
18: public IUnityContainer UnityContainer { get; private set;}
19:
20: public UnityServiceBehaviorAttribute()
21: : this(string.Empty)
22: { }
23:
24: public UnityServiceBehaviorAttribute(string containerName)
25: {
26: containerName = containerName ?? string.Empty;
27: if (containers.ContainsKey(containerName))
28: {
29: this.UnityContainer = containers[containerName];
30: }
31: else
32: {
33: lock (typeof(UnityServiceBehaviorAttribute))
34: {
35: IUnityContainer container = new UnityContainer();
36: UnityConfigurationSection configuration = (UnityConfigurationSection)ConfigurationManager.GetSection(UnityConfigurationSection.SectionName);
37: if (containerName == string.Empty)
38: {
39: configuration.Configure(container);
40: }
41: else
42: {
43: configuration.Configure(container, containerName);
44: }
45: containers[containerName] = container;
46: this.UnityContainer = container;
47: }
48: }
49: }
50: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) {}
51: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
52: {
53: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
54: {
55: foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
56: {
57: Type contractType = (from endpoint in serviceHostBase.Description.Endpoints
58: where endpoint.Contract.Name == endpointDispatcher.ContractName && endpoint.Contract.Namespace == endpointDispatcher.ContractNamespace
59: select endpoint.Contract.ContractType).FirstOrDefault();
60: if (null == contractType)
61: {
62: continue;
63: }
64: if (!this.UnityContainer.Registrations.Any(registration => registration.RegisteredType == contractType))
65: {
66: this.UnityContainer.RegisterType(contractType, serviceHostBase.Description.ServiceType);
67: }
68: endpointDispatcher.DispatchRuntime.InstanceProvider = new UnityInstanceProvider(this.UnityContainer, contractType);
69: }
70: }
71: }
72: public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) {}
73: }
74: }
步骤三、自定义ServiceHost:UnityServiceHost
接下来,我们需要自定义一个ServiceHost来应用上面定义的服务行为UnityServiceBehaviorAttribute。我们将这个自定义的ServiceHost命名为UnityServiceHost,下面的代码片断给出了整个UnityServiceHost的定义。
1: using System;
2: using System.ServiceModel;
3: namespace Artech.WcfExtensions.IoC
4: {
5: public class UnityServiceHost : ServiceHost
6: {
7: private string containerName;
8: public UnityServiceHost(Type serviceType, string containerName, params Uri[] baseAddresses)
9: : base(serviceType, baseAddresses)
10: {
11: this.containerName = containerName;
12: }
13: protected override void OnOpening()
14: {
15: base.OnOpening();
16: if (this.Description.Behaviors.Find<UnityServiceBehaviorAttribute>() == null)
17: {
18: this.Description.Behaviors.Add(new UnityServiceBehaviorAttribute(containerName));
19: }
20: }
21: }
22: }
UnityServiceHost直接继承自ServiceHost。在构造函数中,除了指定服务类型和可选的基地址数组之外,我们还指定了Unity容器的配置名称。在重写的OnOpening方法中,服务行为UnityServiceBehaviorAttribute被创建出来并被添加到了服务行为列表之中。
步骤四、自定义ServiceHostFactory:UnityServiceHostFactory
自定义的ServiceHost在进行IIS或者WAS寄宿的时候需要通过相应的ServiceHostFactory来创建,我们需要为自定义的UnityServiceHost创建对应的UnityServiceHostFactory。
UnityServiceHost具有三个参数:服务类型、Unity容器名称和基地址数组。而通过上面的介绍我们知道最终作为构造函数参数的来源是.svc文件%@ServiceHost%指令的Service属性。为了上该属性能够同时包含用于创建自定义UnityServiceHost必须的服务类型和Unity容器名称,我们希望该属性具有如下的格式,及前半部分代表服务类型,后半部分代表Unity容器名称,中间采用分隔符“:”将两者隔开。
1: <%@ ServiceHost Service="{服务类型}:{Unity容器名称}" ... %>
基于这样的%@ServiceHost%指令的Service属性格式,我们就可以将自定义的UnityServiceHostFactory定义成如下的样子:UnityServiceHostFactory直接继承自ServiceHostFactory,重写的共有CreateServiceHost方法中,将Unity容器名称从constructorString参数中提取出来,并传入只包含服务类型名称的字符串作为参数调用基类的CreateServiceHost方法。而在重写的受保护CreateServiceHost方法中,则根据之前提取出来的Unity容器名称创建UnityServiceHost对象。
1: using System;
2: using System.ServiceModel;
3: using System.ServiceModel.Activation;
4: namespace Artech.WcfExtensions.IoC
5: {
6: public class UnityServiceHostFactory : ServiceHostFactory
7: {
8: public string ContianerName { get; private set; }
9:
10: protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
11: {
12: return new UnityServiceHost(serviceType, this.ContianerName, baseAddresses);
13: }
14: public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
15: {
16: var split = constructorString.Split(':');
17: constructorString = split[0];
18: if (split.Length > 1)
19: {
20: this.ContianerName = split[1].Trim();
21: }
22: return base.CreateServiceHost(constructorString, baseAddresses);
23: }
24: }
25: }
步骤五、创建实例程序应用自定义ServiceHost
最后我们创建一个实例程序来演示如何以IIS寄宿方式使用上面我们自定义ServiceHost。我们依然沿用之前演示的资源服务的例子。在前面演示的例子(《通过“四大行为”对WCF的扩展[实例篇]》)中,我们直接通过获取定义在资源文件(.resx)的方式提供服务的实现。现在我们从可扩展性的角度对服务进行重新设计以实现对不同资源存储方法的支持。也就是说,我可以将资源信息定义在资源文件中,也可能定义在数据库中,或者说访问另一个服务来提供你所需要的资源。
为了让我们的资源服务具有这样的可扩展性,我们将基于不同资源存储方法的功能定义在一个接口中,并将其命名为IResourceProvider。如下面的代码所示,IResourceProvider接口仅仅具有一个GetString方法。
1: public interface IResourceProvider
2: {
3: string GetString(string key);
4: }
然后我们将基于.resx文件的资源提供的功能定义成一个实现了IResourceProvider接口的ResxFileProvider。我们直接将定义在ResourceService的GetString方法中的代码移到了ResxFileProvider的GetString方法中。
1: public class ResxFileProvider : IResourceProvider
2: {
3: public string GetString(string key)
4: {
5: return Resources.ResourceManager.GetString(key);
6: }
7: }
接下来,为了创建于具体资源存储无关的资源服务,我们需要让ResourceService仅仅依赖我们定义的IResourceProvider,下面是ResourceService的定义。
1: public class ResourceService : IResourceService
2: {
3: public IResourceProvider Provider { get; private set; }
4:
5: public ResourceService(IResourceProvider provider)
6: {
7: this.Provider = provider;
8: }
9: public string GetString(string key)
10: {
11: return this.Provider.GetString(key);
12: }
13: }
为了演示IIS服务寄宿方式,我们在IIS管理器创建一个Web应用(比如将其命名为WcfServices)并将其物理路径映射成定义ResourceService项目的根目录。同时更改项目的编译输出目录,从默认的\bin\debug切换成\bin(因为Web应用自动加载的是\bin目录下的程序集)。然后为该项目添加一个Web.config,并进行如下的配置。通过这个配置文件,我们定义了一个名称为defaultContainer的Unity容器,并在该容器中定义了从IResourceProvider接口到ResxFileProvider类型的类型注册。
1: <?xml version="1.0"?>
2: <configuration>
3: <configSections>
4: <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/>
5: </configSections>
6: <unity>
7: <containers>
8: <container name="defaultContainer">
9: <register type="Artech.WcfServices.Servicies.IResourceProvider, Artech.WcfServices.Servicies"
10: mapTo="Artech.WcfServices.Servicies.ResxFileProvider, Artech.WcfServices.Servicies">
11: </register>
12: </container>
13: </containers>
14: </unity>
15: <system.serviceModel>
16: <services>
17: <service name="Artech.WcfServices.Servicies.ResourceService">
18: <endpoint binding="ws2007HttpBinding" contract="Artech.WcfServices.Contracts.IResourceService"/>
19: </service>
20: </services>
21: </system.serviceModel>
22: </configuration>
接下来我们需要在项目的根目录中添加一个文件名为ResourceService.svc的文件,其<%@ServiceHost%>指令定义如下。Service属性以“:”作为分隔符将代表服务类型和Unity容器名称分开,而Factory属性指定的正是用于创建自定义UnityServiceHost的UnityServiceHostFactory的类型。
1: <%@ ServiceHost Service="Artech.WcfServices.Servicies.ResourceService: defaultContainer"
2: Factory="Artech.WcfExtensions.IoC.UnityServiceHostFactory, Artech.WcfExtensions.Lib"%>
现在你只需要将客户端配置中终结点地址替换成ResourceService.svc的地址(http://localhost/wcfservices/ResourceService.svc),你就可以直接运行客户端程序并得到与之前一样的输出结果。
1: <?xml version="1.0"?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint name ="resourceservice"
6: address = "http://localhost/wcfservices/ResourceService.svc"
7: binding ="ws2007HttpBinding"
8: contract ="Artech.WcfServices.Contracts.IResourceService"/>
9: </client>
10: </system.serviceModel>
11: </configuration>
输出结果:
1: Happy New Year!
2: Merry Christmas!
3:
4: 新年快乐!
5: 圣诞快乐!