ASP.NET的输出缓存(Output Caching)机制允许我们针对整个Web页面或者页面的某个部分(主要针对用户控件)最终呈现的HTML进行缓存。对于后续针对相同资源的请求,只需要直接将缓存的HTML予以回复而无须按照页面处理生命周期对每次请求进行重复处理。WCF通过操作行为AspNetCacheProfileAttribute利用ASP.NET的输出缓存提供一种针对于某个操作的声明式缓存机制。[源代码从这里下载]
WCF对ASP.NET缓存的支持是通过AspNetCacheProfileAttribute特性来实现的。通过如下的代码我们不难看出AspNetCacheProfileAttribute是实现了IOperationBehavior接口的操作行为,我们可以直接将其应用到契约接口/类中的某个具有缓存需要的操作方法上。
1: [AttributeUsage(AttributeTargets.Method)]
2: public sealed class AspNetCacheProfileAttribute : Attribute, IOperationBehavior
3: {
4: //其他成员
5: public AspNetCacheProfileAttribute(string cacheProfileName);
6: public string CacheProfileName { get; }
7: }
AspNetCacheProfileAttribute构造函数参数cacheProfileName表示的CacheProfile的配置名称,目标操作按照定义在相应CacheProfile的缓存策略实施缓存。CacheProfile配置在<system.web>/<caching>/<outputCacheSettings>/<outputCacheProfiles>节点下。
1: <configuration>
2: <connectionStrings>
3: <add name="localDb"
4: connectionString="Server=.; Database=TestDb; Uid=sa; Pwd=password"
5: providerName="System.Data.SqlClient"/>
6: </connectionStrings>
7: <system.web>
8: <caching>
9: <outputCacheSettings>
10: <outputCacheProfiles>
11: <add name="default"
12: duration="60"
13: varyByParam="none"
14: sqlDependency="TestDb: TestTable"/>
15: </outputCacheProfiles>
16: </outputCacheSettings>
17: <sqlCacheDependency>
18: <databases>
19: <add name="TestDb" connectionStringName="localDb"/>
20: </databases>
21: </sqlCacheDependency>
22: </caching>
23: </system.web>
24: </configuration>
在如上所示的配置片断中,我们定义了一个名称为default的CacheProfile。代表缓存时间的duration属性被设置为60,意味着缓存项在被存储之后1分钟之后实失效;属性varyByParam被设置为none表示缓存项与请求的查询字符串无关。此外,该CacheProfile还设置针对某个本地数据库中的TestTable表的SQL依赖(SQL Dependency)。关于CacheProfile的配置属于ASP.NET的范畴,在这里我们不会作过多的讨论。
既然是采用ASP.NET输出缓存,WCF服务自然需要采用IIS寄宿并采用ASP.NET 兼容模式。值得一提的是,基于AspNetCacheProfileAttribute的输出缓存仅仅针对HTTP-GET。
接下来我们通过一个简单的实例来演示如何通过操作行为对某个操作的返回值实施缓存,为此我们创建一个用于返回当前时间的服务。如下所示的是作为服务契约的ITime接口的定义,AspNetCacheProfileAttribute特性被应用到了用于返回当前时间的操作方法GetCurrentTime上。
1: using System;
2: using System.ServiceModel;
3: using System.ServiceModel.Web;
4: namespace Artech.WcfServices.Service.Interface
5: {
6: [ServiceContract(Namespace = "http://www.artech.com/")]
7: public interface ITime
8: {
9: [WebGet(UriTemplate = "/current")]
10: [AspNetCacheProfile("default")]
11: DateTime GetCurrentTime();
12: }
13: }
实现了契约接口ITime的服务类型TimeService定义如下。我们将AspNetCompatibilityRequirementsAttribute特性应用在服务类型上并将RequirementsMode属性设置为Allowed以提供对ASP.NET兼容模式的支持。
1: using System;
2: using System.ServiceModel.Activation;
3: using Artech.WcfServices.Service.Interface;
4: namespace Artech.WcfServices.Service
5: {
6: [AspNetCompatibilityRequirements(
7: RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
8: public class TimeService : ITime
9: {
10: public DateTime GetCurrentTime()
11: {
12: return DateTime.Now;
13: }
14: }
15: }
在一个Web项目中我们为通过IIS寄宿的服务TimeService添加一个对应的.svc文件(TimeService.svc),如下所示的是<%@ServiceHost%>指令的定义。表示ServiceHostFactory类型的指令属性Factory被设置为System.ServiceModel.Activation.WebServiceHostFactory.
1: <%@ ServiceHost Service="Artech.WcfServices.Service.TimeService" Factory="System.ServiceModel.Activation.WebServiceHostFactory"%>
我们在作为服务宿主的Web项目下添加一个配置文件(Web.config)并定义如下的配置。除了服务寄宿的基本配置外,我们将<system.serviceModel>/<serviceHostingEnvironment >配置节的aspNetCompatibilityEnabled属性设置为True以开启ASP.NET兼容模式。应用在操作方法GetCurrentTime上的AspNetCacheProfileAttribute特性中指定的名称为default的CacheProfile定义在该配置中,duration和varyByParam分别被设置为60和none。
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Service.TimeService">
5: <endpoint binding="webHttpBinding"
6: contract="Artech.WcfServices.Service.Interface.ITime"/>
7: </service>
8: </services>
9: <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
10: </system.serviceModel>
11: <system.web>
12: <caching>
13: <outputCacheSettings>
14: <outputCacheProfiles>
15: <add name="default" duration="60" varyByParam="none"/>
16: </outputCacheProfiles>
17: </outputCacheSettings>
18: </caching>
19: </system.web>
20: </configuration>
作为客户端的控制台程序在进行了相应配置之后通过如下的代码进行服务的调用。在这段代码中,我们通过创建的服务代理进行了5次服务调用,并将获取的时间打印出来。每次服务的时间间隔为1秒。
1: using (ChannelFactory<ITime> channelFactory = new ChannelFactory<ITime>("timeService"))
2: {
3: ITime proxy = channelFactory.CreateChannel();
4: for (int i = 0; i < 5; i++)
5: {
6: Console.WriteLine(proxy.GetCurrentTime().ToLongTimeString());
7: Thread.Sleep(1000);
8: }
9: }
客户端代码执行之后会在控制台上输出如下的结果。由于服务端通过ASP.NET的输出缓存对第一次执行GetCurrentTime操作的结果进行了缓存,所以客户端返回的时间都是相同的。
1: 4:48:43 PM
2: 4:48:43 PM
3: 4:48:43 PM
4: 4:48:43 PM
5: 4:48:43 PM
既然我们采用ASP.NET兼容模式来寄宿服务,意味着我们调用某个服务与访问某个页面没有本质的区别,所以基于Web页面的输出缓存能够应用于基于某个服务操作的调用就不足为奇了。现在有这么一个问题:通过AspNetCacheProfileAttribute特性指定CacheProfile是如何生效的?
如果对ASP.NET具有一定的了解,应该知道可以通过当前HttpResponse(HttpContext.Current.Response)的Cache属性表示的HttpCachePolicy对象来控制当前输出缓存的基本策略。实际上AspNetCacheProfileAttribute就是通过这种方式将定义在指定CacheProfile的缓存策略应用到针对当前操作的调用上的。
具体来说,AspNetCacheProfileAttribute针对输出缓存策略的控制是通过一个实现了接口IParameterInspector的自定义参数检验器实现的,这是一个名称为CachingParameterInspector的内部类型。操作行为AspNetCacheProfileAttribute通过实现的ApplyDispatchBehavior方法将针对某个CacheProfile创建的CachingParameterInspector对象添加到当前分发操作(DispatchOperation)的参数检验器列表中。
1: internal class CachingParameterInspector : IParameterInspector
2: {
3: public CachingParameterInspector(string cacheProfileName);
4: public object BeforeCall(string operationName, object[] inputshens);
5: public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
6: {
7: //将指定CacheProfile的输出缓存策略应用到当前HttpResponse
8: }
9: }
如上面的代码片断所示,当AfterCall方法被执行的之后,在构造函数中指定的CacheProfile定义的输出缓存策略应用到当前HttpResponse。而AfterCall会在操作执行之后,回复消息序列化之前被执行。