Paolo Salvatori's Blog

Adventures in the magic world of Windows Azure

How to use a WCF custom channel to implement client-side caching

How to use a WCF custom channel to implement client-side caching

Rate This
  • Comments 13

Introduction

A couple of months ago Yossi Dahan told me that one of his customers in the UK was searching for a solution to transparently cache the response messages resulting from a WCF call. I immediately thought that this design pattern could be implemented using a custom channel so I proposed this solution to Yossi. So I sent him the code of a custom WCF channel that I built for another project, he created a first prototype to test the feasibility of the outlined approach, then I extended the component to include the support for Windows Server AppFabric Caching and many additional features that I’ll explain in this article.

The idea of using Windows Server AppFabric Caching to manage caching is not new, but all the samples I saw so far on internet implement a server side caching using an a custom component that implements the IOperationInvoker standard interface. Conversely, my component implements a client-side caching using a custom protocol channel. Moreover, my extensions library provides the possibility to choose among three caching providers:

  • A Memory Cache-based provider: this component doesn’t need the installation of Windows Server AppFabric Caching as it internally uses an instance of the MemoryCache class contained in the .Net Framework 4.0.
  • A Web Cache-based provider: this component, as the previous one, doesn’t need the installation of Windows Server AppFabric Caching as it internally uses an instance of the Cache class supplied by ASP.NET.
  • An AppFabric Caching provider: as the name suggests, this caching provider requires and leverages Windows Server AppFabric Caching. To further improve the performance, it’s highly recommended the client application to use the Local Cache to store response messages in-process. 

Client-side  caching and server-side caching are two powerful and complimentary techniques to improve the performance of a server application.  Client-caching is particularly indicated for those applications, like a web site, that frequently invoke one or multiple back-end systems to retrieve reference and lookup data, that is data that is static and change quite rarely. By using client-side caching you avoid making redundant calls to retrieve the same data, especially when the calls in question take a time to complete. My component allows to extend existing server applications with client-caching capabilities without the need to change their code to explicitly use the functionality supplied by Windows Server AppFabric Caching.

For more information on how to implement server side caching, you can review the following articles:

  • Use AppFabric Cache to cache your WCF Service response” article on Mikael Håkansson's Blog.
  • A Configurable AppFabric Cache Attribute For Your WCF Services” article by Christian Martinez.
  • WCF AppFabric Caching Behavior Sample” sample by Ron Jacobs.

WCF Messaging Runtime

Before diving into the code, let's do a quick excursion on how the WCF messaging actually works. The WCF runtime is divided into 2 primary layers as shown by the following picture:

  • The Service Layer aka Service Model defines the mechanisms and attributes used by developers to define and decorate service, message and data contracts.
  • The  Messaging Layer  is instead responsible for preparing a WCF message for transmission on the send side and produce a WCF message for the dispatcher on the receive side. The messaging layer accomplishes this task using a Channel Stack. This latter is a pipeline of channel components that handle different processing tasks.  Each channel stack is composed of exactly one transport channel, one message encoder, and zero or more protocol channels.

WCFRuntime

It’s the responsibility of the proxy component on the client side and dispatcher component on the service side to mediate and translate between the two layers. In particular, the proxy component transforms .NET method calls into Message objects, whereas the dispatcher component turns WCF Messages into .NET method calls. WCF uses the Message class to model all incoming/outgoing messages within the Messaging Layer. The message represents a a SOAP Envelope, and therefore it’s composed of a payload and a set of headers. A typical WCF communication can be described as follows:

  1. The client application creates one or more input parameters. Each of these parameters is defined by a data contract.
  2. The client application invokes one of the methods of the service contract exposed by the proxy.
  3. The proxy delivers a WCF Message object to the channel stack.
  4. At this point each protocol channel has a chance to operate on the message before the transport channel uses a message encoder to transmit the final Message as a sequence of bytes to the target service. Each protocol channel can modify the content or the headers of the message to implement specific functionalities or WS-* protocols like WS-AtomicTransaction, WS-Security.
  5. The raw stream of data is transmitted over the wire.
  6. On the service side, the transport channel receives the stream of data and uses a message encoder to interpret the bytes and to produce a WCF Message object that can continue up the channel stack. At this point each protocol channel has a chance to work on the message.
  7. The final Message is passed to the Dispatcher.
  8. The Dispatcher receives the WCF Message from the underlying channel stack, individuates the target service endpoint using the destination address and Action property contained in the Message, deserializes the content of the WCF Message into objects.
  9. Finally the target service method is invoked.

After a slightly long-winded but necessary introduction, we are now ready to introduce the problem statement and examine how to leverage my component in three different application scenarios.

Problem Statement

The problem statement that my component intends to solve can be formulated as follows:

  • How can I implicitly cache response messages within a consumer application that invokes one or multiple underlying services using WCF and a Request-Response message exchange pattern without modifying the code of the application in question?

To solve this problem, I created a custom protocol channel that you can explicitly or implicitly use inside a CustomBinding when specifying client endpoints within the configuration file or by code using the WCF API.

Scenarios

The design pattern implemented by my component can be described as follows: a client application submits a request to WCF service hosted in IIS\AppFabric and waits for a response. The service invoked by the client application uses a WCF proxy to invoke a back-end service. My custom channel is configured to run first in the channel stack. It checks the presence of the response message in the cache and behaves accordingly:

  • If the response message is in the cache, the custom channel immediately returns the response message from the cache without invoking the underlying service.
  • Conversely, if the response message is not in the cache, the custom channel calls the underlying channel to invoke the back-end service and then caches the response message using the caching provider defined in the configuration file for the actual call.

 

First Scenario

The following picture depicts the architecture of the first scenario that uses the AppFabric Caching provider to cache response message in the AppFabric local and distributed cache.

 

AppFabricCache

Message Flow

  1. The client application submits a request to a WCF service and waits for a response.
  2. The WCF Service invokes one the methods exposed by the WCF proxy object.
  3. The proxy transforms the .NET method call into a WCF message and delivers it to the underlying channel stack.
  4. The caching channel checks the presence of the response message in the AppFabric Caching Local Cache or on Cache Cluster. If the service in question is hosted by a web farm, the response message may have been previously put in the distributed cache by another service instance running on the same machine or on another node of the farm. If the caching channel finds the response message for the actual call in the local or distributed cache, it immediately returns this message to the proxy object without invoking the back-end service.
  5. Conversely, if the response message is not in the cache, the custom channel calls the underlying channel to invoke the back-end service and then caches the response message using the AppFabric Caching provider.
  6. The caching channel returns the response WCF message to the proxy.
  7. The proxy transforms the WCF message into a response object.
  8. The WCF service creates and returns a response message to the client application.

 

Second Scenario

The following diagram shows the architecture of the second scenario. In this case, the service uses the Memory Cache or the Web Cache provider, therefore each node of the web farm has a private copy of the response messages.

WebCache

Message Flow

  1. The client application submits a request to a WCF service and waits for a response.
  2. The WCF Service invokes one the methods exposed by the WCF proxy object.
  3. The proxy transforms the .NET method call into a WCF message and delivers it to the underlying channel stack.
  4. The caching channel checks the presence of the response message in the in-process Memory or Web Cache and, in affirmative case, it returns it to the proxy object without invoking the back-end service.
  5. Conversely, if the response message is not in the cache, the custom channel calls the underlying channel to invoke the back-end service and then caches the response message in the Memory or Web Cache.
  6. The caching channel returns the response WCF message to the proxy.
  7. The proxy transforms the WCF message into a response object.
  8. The WCF service creates and returns a response message to the client application.

 

Third Scenario

Finally, the following figure shows how to get advantage of my component in a BizTalk Server application:

BizTalk

Message Flow

  1. The client application submits a request to a WCF receive location and waits for a response.
  2. The XML disassembler component within the XmlReceive pipeline recognizes the document type and promotes the MessageType context property.
  3. The Message Agent publishes the document to the MessageBox database.
  4. The inbound request starts a new instance of an given orchestration.
  5. The orchestration posts to the MessageBox database a request message for a back-end service.
  6. The request message is processed by a WCF-Custom send port which is configured to use the CustomBinding. In particular, the binding is composed by a transport binding element, by a message encoder, and by one or multiple protocol binding elements. The first of these components is the Binding Element that at runtime is responsible for creating the ChannelFactory which in turns creates the caching channel.
  7. The WCF-Custom Adapter transforms the IBaseMessage into a WCF Message and relay it to the channel stack.
  8. The caching channel checks the presence of the response message in the local or distributed cache. In affirmative case, it retrieves the response message from the cache and returns it to the WCF-Custom Adapter without invoking the back-end service. Conversely, if the response message is not in the cache, the custom channel calls the underlying channel to invoke the back-end service and then caches the response message in the both the local and distributed cache. The WCF-Custom Adapter transforms the WCF Message into a IBaseMessage.
  9. The WCF send port publishes the message to the MessageBox database.
  10. The orchestration consumes the response message and prepares a response message for the client application.
  11. The orchestration publishes the response message to the MessageBox database.
  12. The response message is retrieved by the WCF receive location.
  13. The response message is returned to the client application.

We are now ready to analyze the code.

The Solution

The code has been realized in C# using Visual Studio 2010 and the .NET Framework 4.0. The following picture shows the projects that comprise the WCFClientCachingChannel solution:

Solution

The following is a brief description of individual projects:

  • AppFabricCache: this caching provider implements the Get and Put methods to retrieve and store data items to the AppFabric local and distributed cache.
  • WebCache: this caching provider provides the Get and Put methods to retrieve and store items to a static in-process Web Cache object.
  • ExtensionLibrary: this assembly contains the WCF extensions to configure, create and run the caching channel at runtime.
  • Helpers: this library contains the helper components used by the WCF extensions objects to handle exceptions and trace messages.
  • Scripts: this folder contains the scripts to create a named cache in Windows Server AppFabric Caching and the scripts to start and stop both the cache cluster and individual cache hosts.
  • Tests: this test project contains the unit and load tests that I built to verify the runtime behavior of my component.
  • TestServices: this project contains a console application that opens and exposes a test WCF service.

Configuration

The following table shows the app.config configuration file of the Tests project.

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <!--configSections must be the FIRST element -->
   4:   <configSections>
   5:     <!-- required to read the <dataCacheClient> element -->
   6:     <section name="dataCacheClient"
   7:        type="Microsoft.ApplicationServer.Caching.DataCacheClientSection, Microsoft.ApplicationServer.Caching.Core, 
Version=1.0.0.0,Culture=neutral, PublicKeyToken=31bf3856ad364e35"
   8:        allowLocation="true"
   9:        allowDefinition="Everywhere"/>
  10:   </configSections>
  11:  
  12:   <dataCacheClient>
  13:     <!-- (optional) Local Cache Settings -->
  14:     <localCache  isEnabled="true" sync="TimeoutBased" objectCount="100000" ttlValue="300" />
  15:     <!-- Security Settings -->
  16:     <securityProperties mode="None" protectionLevel="None" />
  17:     <!-- Hosts -->
  18:     <hosts>
  19:       <host
  20:          name="localhost" cachePort="22233"/>
  21:     </hosts>
  22:     <!-- Transport Properties -->
  23:     <transportProperties connectionBufferSize="131072"
  24:                          maxBufferPoolSize="268435456"
  25:                          maxBufferSize="8388608"
  26:                          maxOutputDelay="2"
  27:                          channelInitializationTimeout="60000"
  28:                          receiveTimeout="600000"/>
  29:   </dataCacheClient>
  30:  
  31:   <system.serviceModel>
  32:  
  33:     <client>
  34:       <endpoint address="http://localhost:8732/TestService/"
  35:                 binding="customBinding"
  36:                 bindingConfiguration="customBinding"
  37:                 contract="TestServices.ITestService"
  38:                 name="CustomBinding_ITestService" />
  39:       <endpoint address="http://localhost:8732/TestService/"
  40:                 binding="basicHttpBinding"
  41:                 behaviorConfiguration="basicHttpBinding"
  41:                 bindingConfiguration="basicHttpBinding"
  42:                 contract="TestServices.ITestService"
  43:                 name="BasicHttpBinding_ITestService" />
  44:     </client>
  45:  
  46:     <bindings>
  47:       <customBinding>
  48:         <binding name="customBinding"
  49:                  closeTimeout="00:10:00"
  50:                  openTimeout="00:10:00"
  51:                  receiveTimeout="00:10:00"
  52:                  sendTimeout="00:10:00">
  53:           <clientCaching enabled="true"
  54:                          header="true"
  55:                          timeout="00:05:00"
  56:                          cacheType="AppFabricCache"
  57:                          cacheName="WCFClientCache"
  58:                          regionName="Messages"
  59:                          maxBufferSize="65536"
  60:                          keyCreationMethod="Simple">
  61:             <operations>
  62:               <operation action="TestAction"
  63:                          keyCreationMethod="Action"
  64:                          cacheType="AppFabricCache"
  65:                          timeout="00:20:00" />
  66:               <operation action="TestSimple"
  67:                          keyCreationMethod="Simple"
  68:                          cacheType="AppFabricCache"
  69:                          timeout="00:20:00" />
  70:               <operation action="TestMD5"
  71:                          keyCreationMethod="MD5"
  72:                          cacheType="WebCache"
  73:                          timeout="00:10:00" />
  74:               <operation action="TestMessageBody"
  75:                          keyCreationMethod="MessageBody"
  76:                          cacheType="AppFabricCache"
  77:                          timeout="00:20:00" />
  78:               <operation action="TestIndexed"
  79:                          keyCreationMethod="Indexed"
  80:                          cacheType="AppFabricCache"
  81:                          timeout="00:20:00"
  82:                          indexes="1,2" />
  83:             </operations>
  84:           </clientCaching>
  85:           <textMessageEncoding messageVersion="Soap11" />
  86:           <httpTransport />
  87:         </binding>
  88:       </customBinding>
  89:       <basicHttpBinding>
  90:         <binding name="basicHttpBinding"  
  91:                  closeTimeout="00:10:00" 
  92:                  openTimeout="00:10:00" 
  93:                  receiveTimeout="00:10:00" 
  94:                  sendTimeout="00:10:00" 
  95:                  allowCookies="false" 
  96:                  bypassProxyOnLocal="false" 
  97:                  hostNameComparisonMode="StrongWildcard" 
  98:                  maxBufferSize="10485760" 
  99:                  maxBufferPoolSize="1048576" 
 100:                  maxReceivedMessageSize="10485760" 
 101:                  messageEncoding="Text" 
 102:                  textEncoding="utf-8" 
 103:                  transferMode="Buffered" 
 104:                  useDefaultWebProxy="true">
 105:         </binding>
 106:       </basicHttpBinding>
 107:     </bindings>
 108:  
 109:     <extensions>
 110:       <behaviorExtensions>
 111:         <add name="cachingBehavior" 
             type="Microsoft.AppFabric.CAT.Samples.WCF.ClientCache.ExtensionLibrary.ClientCacheBehaviorExtensionElement,
Microsoft.AppFabric.CAT.Samples.WCF.ClientCache.ExtensionLibrary,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=8f6257ebc688af7c"
/>
 112:       </behaviorExtensions>
 113:       <bindingElementExtensions>
 114:         <!-- This item is required to register our custom binding element -->
 115:         <add name="clientCaching" 
              type="Microsoft.AppFabric.CAT.Samples.WCF.ClientCache.ExtensionLibrary.ClientCacheBindingExtensionElement,
Microsoft.AppFabric.CAT.Samples.WCF.ClientCache.ExtensionLibrary,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=8f6257ebc688af7c"
/>
 116:       </bindingElementExtensions>
 117:     </extensions>
 118:  
 119:     <behaviors>
 120:       <endpointBehaviors>
 121:         <!—- basicHttpBinding Endpoint Behavior -->
 122:         <behavior name="basicHttpBinding">
 123:           <cachingBehavior enabled="true"
 124:                            header="true"
 125:                            timeout="00:05:00"
 126:                            cacheType="AppFabricCache"
 127:                            cacheName="WCFClientCache"
 128:                            regionName="Messages"
 129:                            maxBufferSize="65536"
 130:                            keyCreationMethod="Simple">
 131:             <operations>
 132:               <operation action="TestAction"
 133:                          keyCreationMethod="Action"
 134:                          cacheType="AppFabricCache"
 135:                          timeout="00:20:00" />
 136:               <operation action="TestSimple"
 137:                          keyCreationMethod="Simple"
 138:                          cacheType="AppFabricCache"
 139:                          timeout="00:20:00" />
 140:               <operation action="TestMD5"
 141:                          keyCreationMethod="MD5"
 142:                          cacheType="WebCache"
 143:                          timeout="00:10:00" />
 144:               <operation action="TestMessageBody"
 145:                          keyCreationMethod="MessageBody"
 146:                          cacheType="AppFabricCache"
 147:                          timeout="00:20:00" />
 148:               <operation action="TestIndexed"
 149:                          keyCreationMethod="Indexed"
 150:                          cacheType="AppFabricCache"
 151:                          timeout="00:20:00"
 152:                          indexes="1,2" />
 153:             </operations>
 154:           </cachingBehavior>
 155:         </behavior>
 156:       </endpointBehaviors>
 157:     </behaviors>
 158:   </system.serviceModel>
 159: </configuration>

 

Please find below a brief description of the main elements and sections of the configuration file:

  • Lines [4-10] define the config sections. For AppFabric caching features to work, the configSections element must be the first element in the application configuration file. It must contain child elements that tell the runtime how to use the dataCacheClient element.
  • Lines [12-29] contain the dataCacheClient element that is used to configure the cache client. Child elements define cache client configuration; in particular, the localCache element specifies the local cache settings, whereas the hosts element defines the DNS name and port of available cache hosts.
  • Lines [33-44] contain the client section that defines a list of endpoints the test project uses to connect to the test service. In particular, I created 2 different endpoints to demonstrate how to configure the caching channel:
    • The first endpoint uses the CustomBinding as a recipe to create the channel stack at runtime. The custom binding is composed of 3 binding elements: the clientCaching, textMessageEncoding and httpTransport. As you can see at lines [47-88], the clientCaching binding element allows to accurately configure the runtime behavior of the caching channel at a general level and on a per operation basis. Below I will explain in detail how to configure the clientCaching binding element.
    • The second endpoint adopts the BasicHttpBinding to communicate with the underlying service. However, the endpoint is configured to use the cachingBehavior that at runtime replaces the original binding with a CustomBinding made up of the same binding elements and adds the clientCaching binding element as the first element to the binding element collection. This technique is an alternative way to use and configure the caching channel.
  • Lines [109-117] contain the extensions element which defines the cachingBehavior extension element and the clientCaching binding element extension element.
  • Lines [122-154] contain the basicHttpBinding endpoint configuration.

As you can easily notice, both the cachingBehavior and clientCaching components share the same configuration that is defined as follows:

cachingBehavior and clientCaching elements:

  • enabled property: gets or sets a value indicating whether the WCF caching channel is enabled. When the value is false, the caching channel always invokes the target service. This property can be overridden at the operation level. This allows to enable or disable caching on a per operation basis.
  • header property: gets or sets a value indicating whether a custom header is added to the response to indicate the source of the WCF message (cache or service). This property can be overridden at the operation level.
  • timeout property: gets or sets the default amount of time the object should reside in the cache before expiration. This property can be overridden at the operation level.
  • cacheType property: gets or sets the cache type used to store items. The component actually supports two caching providers: AppFabricCache and WebCache. This property can be overridden at the operation level.
  • cacheName property: gets or sets the name of the cache used for storing messages in the AppFabric distributed cache. This property is used only when the value of the cacheType property is equal to AppFabricCache.
  • regionName property: gets or sets the name of the region used for storing messages in the AppFabric distributed cache. This property is used only when the value of the cacheType property is equal to AppFabricCache. If the value of this property is null or empty, the component will not use any named region.
  • maxBufferSize property: gets or sets the maximum size in bytes for the buffers used by the caching channel. This property can be overridden at the operation level.
  • indexes property: gets or sets a string containing a comma-separated list of indexes of parameters to be used to compute the cache key. This property is used only when the keyCreationMethod = Indexed.
  • keyCreationMethod property: gets or sets the method used to calculate the key for cache items. The component provides 5 key creation methods:
    • Action: this method uses the value of the Action header of the request as key for the response. For obvious reasons, this method can be used only for operations without input parameters.
    • MessageBody: this method uses the body of the request as key for the response. This method doesn’t work when the request message contains contains DateTime elements that could vary from call to call.
    • Simple: this method creates the string [A](P1)(P2)…(Pn) for an operation with n parameters P1-Pn and Action = A.
    • Indexed: this method works as the Simple method, but it allows to specify which parameters to use when creating the key. For example, the Indexed method creates the string [A](P1)(P3)(5) for an operation with n parameters P1-Pn (n >= 5) and Action = A and when the value of the Indexes property is equal to “1, 3, 5”. This method can be used to skip DateTime parameters from the compute of the key.
    • MD5: this method uses the MD5 algorithm to compute a hash from the body of the request message.

operation element:

  • action property: gets or sets the WS-Addressing action of the request message.
  • enabled property: gets or sets a value indicating whether the WCF caching channel is enabled for the current operation identified by the Action property.
  • header property: gets or sets a value indicating whether a custom header is added to the response to indicate the source of the WCF message (cache or service) at the operation level.
  • timeout property: gets or sets the default amount of time the object should reside in the cache before expiration at the operation level.
  • cacheType property: gets or sets  the cache type used to store responses for the current operation. The component actually supports two caching providers: AppFabricCache and WebCache. This property can be overridden at the operation level.
  • maxBufferSize property: gets or sets the maximum size in bytes for the buffers used by the caching channel for the current operation.
  • indexes property: gets or sets a string containing a comma-separated list of indexes of parameters to be used to compute the cache key for the current operation. This property is used only when the keyCreationMethod = Indexed.
  • keyCreationMethod property: gets or sets the method used to calculate the key for cache items.
  •  

Source Code

This section contains the code of the main classes of the solution. You can find the rest of the components in the source code that accompanies this article. The following table contains the source code for the Cache class in the AppFabricCache project. This class implements the AppFabric caching provider used by the caching channel.

public static class Cache
{
#region Private Static Fields
private static DataCache dataCache;
#endregion

#region Static Constructor
static Cache()
{
try
{
DataCacheFactoryConfiguration dataCacheFactoryConfiguration = new DataCacheFactoryConfiguration();
DataCacheFactory dataCacheFactory = new DataCacheFactory(dataCacheFactoryConfiguration);
dataCache = null;
if (!string.IsNullOrEmpty(CacheSettings.CacheName))
{
dataCache = dataCacheFactory.GetCache(CacheSettings.CacheName);
}
else
{
dataCache = dataCacheFactory.GetDefaultCache();
}
if (dataCache !=null &&
!string.IsNullOrEmpty(CacheSettings.RegionName))
{
dataCache.CreateRegion(CacheSettings.RegionName);
}
}
catch (Exception ex)
{
ExceptionHelper.HandleException(ex);
}
}
#endregion

#region Public Static Methods
/// <summary>
/// Adds an object to the cache.
/// </summary>
/// <typeparam name="T">Type of item being added.</typeparam>
/// <param name="key">The unique value that is used to identify the object in the cache.</param>
/// <param name="value">The object saved to the cache cluster.</param>
/// <param name="timeout">The amount of time the object should reside in the cache before expiration.</param>
public static void Add<T>(string key, T value, TimeSpan timeout)
{
if (value == null)
{
return;
}

if (string.IsNullOrEmpty(CacheSettings.RegionName))
{
dataCache.Put(key, value, timeout);
}
else
{
dataCache.Put(key, value, timeout, CacheSettings.RegionName);
}
}

/// <summary>
/// Used to retrieve an item from the cache.
/// </summary>
/// <typeparam name="T">Type of item being added.</typeparam>
/// <param name="key">The unique value that is used to identify the object in the cache.</param>
/// <returns>An item of type T if found in the cache using the key provided, null otherwise.</returns>
public static T Get<T>(string key)
{
if (string.IsNullOrEmpty(CacheSettings.RegionName))
{
return (T)dataCache.Get(key);
}
else
{
return (T)dataCache.Get(key, CacheSettings.RegionName);
}
}
#endregion
}

 

The table below shows the source code for the Cache class contained in the MemoryCache project. Note: in order to exploit this provider, you need to use  the .NET Framework 4.0.

public static class Cache
{
    #region Private Constants
    private const string ConfigurationName = "WCFMemoryCache";
    private const string RegionName = "Messages";
    private const string WCFMemoryCachePerformanceCounterCategoryName = "WCF Memory Cache";
    private const string WCFMemoryCachePerformanceCounterCategoryDescription = 
"This category contains performance counters exposed by the Memory Cache component."; private const string TotalObjectCountRequestsPerformanceCounterName = "Total Object Count"; private const string TotalObjectCountPerformanceCounterDescription =
"Represents the total number of objects stored in the cache."; private const string TotalClientRequestsPerformanceCounterName = "Total Client Requests"; private const string TotalClientRequestsPerformanceCounterDescription =
"Represents the number of requests served by the cache."; private const string TotalObjectsReturnedPerformanceCounterName = "Total Objects Returned"; private const string TotalObjectsReturnedPerformanceCounterDescription =
"The total number of successful cache requests served by the cache."; private const string TotalGetMissesPerformanceCounterName = "Total Get Misses"; private const string TotalGetMissesPerformanceCounterDescription =
"The total number of unsuccessful cache requests served by the cache."; private const string TotalClientRequestsPerSecPerformanceCounterName = "Total Client Requests/sec"; private const string TotalClientRequestsPerSecPerformanceCounterDescription =
"Represents the number of requests per second served by the cache."; private const string TotalObjectsReturnedPerSecPerformanceCounterName = "Total Objects Returned/sec"; private const string TotalObjectsReturnedPerSecPerformanceCounterDescription =
"The total number of successful cache requests per second served by the cache."; private const string TotalGetMissesPerSecPerformanceCounterName = "Total Get Misses/sec"; private const string TotalGetMissesPerSecPerformanceCounterDescription =
"The total number of unsuccessful cache requests per second served by the cache."; #endregion #region Static Fields //indicates if performance counters set up on server and so should be used. private static bool performanceCountersExist = false; private static PerformanceCounter totalObjectCountPerformanceCounter = null; private static PerformanceCounter totalClientRequestsPerformanceCounter = null; private static PerformanceCounter totalObjectsReturnedPerformanceCounter = null; private static PerformanceCounter totalGetMissesPerformanceCounter = null; private static PerformanceCounter totalClientRequestsPerSecPerformanceCounter = null; private static PerformanceCounter totalObjectsReturnedPerSecPerformanceCounter = null; private static PerformanceCounter totalGetMissesPerSecPerformanceCounter = null; //private static member for the cache store private static System.Runtime.Caching.MemoryCache store =
new System.Runtime.Caching.MemoryCache(ConfigurationName, null); #endregion #region Static Constructor /// <summary> /// static constructor used to initialise performance counters /// </summary> static Cache() { try { if (!PerformanceCounterCategory.Exists(WCFMemoryCachePerformanceCounterCategoryName)) { // Create a collection of type CounterCreationDataCollection. CounterCreationDataCollection counterCreationDataCollection =
new CounterCreationDataCollection(); // Create the counters and set their properties. // Total Object Count CounterCreationData counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalObjectCountRequestsPerformanceCounterName; counterCreationData.CounterHelp = TotalObjectCountPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64; counterCreationDataCollection.Add(counterCreationData); // Total Client Requests counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalClientRequestsPerformanceCounterName; counterCreationData.CounterHelp = TotalClientRequestsPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64; counterCreationDataCollection.Add(counterCreationData); // Total Objects Returned counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalObjectsReturnedPerformanceCounterName; counterCreationData.CounterHelp = TotalObjectsReturnedPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64; counterCreationDataCollection.Add(counterCreationData); // Total Get Misses counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalGetMissesPerformanceCounterName; counterCreationData.CounterHelp = TotalGetMissesPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64; counterCreationDataCollection.Add(counterCreationData); // Total Client Requests/Sec counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalClientRequestsPerSecPerformanceCounterName; counterCreationData.CounterHelp = TotalClientRequestsPerSecPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.RateOfCountsPerSecond64; counterCreationDataCollection.Add(counterCreationData); // Total Objects Returned/Sec counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalObjectsReturnedPerSecPerformanceCounterName; counterCreationData.CounterHelp = TotalObjectsReturnedPerSecPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.RateOfCountsPerSecond64; counterCreationDataCollection.Add(counterCreationData); // Total Get Misses/Sec counterCreationData = new System.Diagnostics.CounterCreationData(); counterCreationData.CounterName = TotalGetMissesPerSecPerformanceCounterName; counterCreationData.CounterHelp = TotalGetMissesPerSecPerformanceCounterDescription; counterCreationData.CounterType = PerformanceCounterType.RateOfCountsPerSecond64; counterCreationDataCollection.Add(counterCreationData); // Create the category and pass the collection to it. PerformanceCounterCategory.Create(WCFMemoryCachePerformanceCounterCategoryName, WCFMemoryCachePerformanceCounterCategoryDescription, PerformanceCounterCategoryType.MultiInstance, counterCreationDataCollection); } // Get the current process Process process = Process.GetCurrentProcess(); string instanceName = string.Format("{0} ({1})", process.ProcessName, process.Id); // Create counters // Total Object Count totalObjectCountPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalObjectCountRequestsPerformanceCounterName, instanceName, false); totalObjectCountPerformanceCounter.RawValue = 0; // Total Client Requests totalClientRequestsPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalClientRequestsPerformanceCounterName, instanceName, false); totalClientRequestsPerformanceCounter.RawValue = 0; // Total Objects Returned totalObjectsReturnedPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalObjectsReturnedPerformanceCounterName, instanceName, false); totalObjectsReturnedPerformanceCounter.RawValue = 0; // Total Get Misses totalGetMissesPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalGetMissesPerformanceCounterName, instanceName, false); totalGetMissesPerformanceCounter.RawValue = 0; // Total Client Requests/Sec totalClientRequestsPerSecPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalClientRequestsPerSecPerformanceCounterName, instanceName, false); totalClientRequestsPerSecPerformanceCounter.RawValue = 0; // Total Objects Returned/Sec totalObjectsReturnedPerSecPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalObjectsReturnedPerSecPerformanceCounterName, instanceName, false); totalObjectsReturnedPerSecPerformanceCounter.RawValue = 0; // Total Get Misses/Sec totalGetMissesPerSecPerformanceCounter =
new PerformanceCounter(WCFMemoryCachePerformanceCounterCategoryName, TotalGetMissesPerSecPerformanceCounterName, instanceName, false); totalGetMissesPerSecPerformanceCounter.RawValue = 0; // If we found one set flag to true, this will be checked later on to determine whether to increment counters performanceCountersExist = true; } catch (Exception ex) { // Failure usually indicates either permission problems or category does not exist, // either way, we log the error and carry on without updating counters ExceptionHelper.HandleException(ex); performanceCountersExist = false; } } #endregion #region Public Static Methods /// <summary> /// Adds an object to the cache. /// </summary> /// <typeparam name="T">Type of item being added.</typeparam> /// <param name="key">The unique value that is used to identify the object in the cache.</param> /// <param name="value">The object saved to the cache cluster.</param> /// <param name="timeout">The amount of time the object should reside in the cache before expiration.</param> public static void Add<T>(string key, T value, TimeSpan timeout) { if (value == null) { return; } if (store[key] != null) { store.Remove(key); } CacheItemPolicy cacheItemPolicy = new CacheItemPolicy(); cacheItemPolicy.SlidingExpiration = timeout; cacheItemPolicy.RemovedCallback = OnRemoveCallBack; store.Add(new CacheItem(key, value, RegionName), cacheItemPolicy); if (performanceCountersExist) { totalObjectCountPerformanceCounter.Increment(); } } /// <summary> /// Used to retrieve an item from the cache. /// </summary> /// <typeparam name="T">Type of item being added.</typeparam> /// <param name="key">The unique value that is used to identify the object in the cache.</param> /// <returns>An item of type T if found in the cache using the key provided, null otherwise.</returns> public static T Get<T>(string key) { if (performanceCountersExist) { totalClientRequestsPerformanceCounter.Increment(); totalClientRequestsPerSecPerformanceCounter.Increment(); } T cacheObject = (T)store.Get(key); if (performanceCountersExist) { if (cacheObject != null) { totalObjectsReturnedPerformanceCounter.Increment(); totalObjectsReturnedPerSecPerformanceCounter.Increment(); } else { totalGetMissesPerformanceCounter.Increment(); totalGetMissesPerSecPerformanceCounter.Increment(); } } return cacheObject; } #endregion #region Private Static Methods /// <summary> /// Defines a reference to a method that is called after a cache entry is removed from the cache. /// </summary> /// <param name="arguments">The information about the cache entry that was removed from the cache.</param> private static void OnRemoveCallBack(CacheEntryRemovedArguments arguments) { if (performanceCountersExist) { totalObjectCountPerformanceCounter.Decrement(); } } #endregion }

 

Finally, the table below shows the source code for the Cache class in the WebCache project. As the name suggests, this class implements the WebCache provider used by the caching channel. Note: this class exposes a set of performance counters that allow to monitor the performance and runtime behavior of this component.

public static class Cache
{
#region Private Constants
private const string WCFWebCachePerformanceCounterCategoryName = "WCF Web Cache";
private const string WCFWebCachePerformanceCounterCategoryDescription =
                                  "This category contains performance counters exposed by the Web Cache component.";
private const string TotalObjectCountRequestsPerformanceCounterName = "Total Object Count";
private const string TotalObjectCountPerformanceCounterDescription =
                                                      "Represents the total number of objects stored in the cache.";
private const string TotalClientRequestsPerformanceCounterName = "Total Client Requests";
private const string TotalClientRequestsPerformanceCounterDescription =
                                                           "Represents the number of requests served by the cache.";
private const string TotalObjectsReturnedPerformanceCounterName = "Total Objects Returned";
private const string TotalObjectsReturnedPerformanceCounterDescription =
                                               "The total number of successful cache requests served by the cache.";
private const string TotalGetMissesPerformanceCounterName = "Total Get Misses";
private const string TotalGetMissesPerformanceCounterDescription =
                                              "The total number of unsuccessful cache requests served by the cache.";
private const string TotalClientRequestsPerSecPerformanceCounterName = "Total Client Requests/sec";
private const string TotalClientRequestsPerSecPerformanceCounterDescription =
                                                 "Represents the number of requests per second served by the cache.";
private const string TotalObjectsReturnedPerSecPerformanceCounterName = "Total Objects Returned/sec";
private const string TotalObjectsReturnedPerSecPerformanceCounterDescription =
                                     "The total number of successful cache requests per second served by the cache.";
private const string TotalGetMissesPerSecPerformanceCounterName = "Total Get Misses/sec";
private const string TotalGetMissesPerSecPerformanceCounterDescription =
                                   "The total number of unsuccessful cache requests per second served by the cache.";
#endregion

#region Static Fields
//indicates if performance counters set up on server and so should be used.
private static bool performanceCountersExist = false;
private static PerformanceCounter totalObjectCountPerformanceCounter = null;
private static PerformanceCounter totalClientRequestsPerformanceCounter = null;
private static PerformanceCounter totalObjectsReturnedPerformanceCounter = null;
private static PerformanceCounter totalGetMissesPerformanceCounter = null;
private static PerformanceCounter totalClientRequestsPerSecPerformanceCounter = null;
private static PerformanceCounter totalObjectsReturnedPerSecPerformanceCounter = null;
private static PerformanceCounter totalGetMissesPerSecPerformanceCounter = null;
//private static member for the cache store
private static System.Web.Caching.Cache store = System.Web.HttpRuntime.Cache;
#endregion

#region Static Constructor
/// <summary>
/// static constructor used to initialise performance counters
/// </summary>
static Cache()
{
try
{
if (!PerformanceCounterCategory.Exists(WCFWebCachePerformanceCounterCategoryName))
{
// Create a collection of type CounterCreationDataCollection.
CounterCreationDataCollection counterCreationDataCollection =
                                                                  new CounterCreationDataCollection();

// Create the counters and set their properties.

// Total Object Count
CounterCreationData counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalObjectCountRequestsPerformanceCounterName;
counterCreationData.CounterHelp = TotalObjectCountPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64;
counterCreationDataCollection.Add(counterCreationData);

// Total Client Requests
counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalClientRequestsPerformanceCounterName;
counterCreationData.CounterHelp = TotalClientRequestsPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64;
counterCreationDataCollection.Add(counterCreationData);

// Total Objects Returned
counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalObjectsReturnedPerformanceCounterName;
counterCreationData.CounterHelp = TotalObjectsReturnedPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64;
counterCreationDataCollection.Add(counterCreationData);


// Total Get Misses
counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalGetMissesPerformanceCounterName;
counterCreationData.CounterHelp = TotalGetMissesPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.NumberOfItems64;
counterCreationDataCollection.Add(counterCreationData);

// Total Client Requests/Sec
counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalClientRequestsPerSecPerformanceCounterName;
counterCreationData.CounterHelp = TotalClientRequestsPerSecPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.RateOfCountsPerSecond64;
counterCreationDataCollection.Add(counterCreationData);

// Total Objects Returned/Sec
counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalObjectsReturnedPerSecPerformanceCounterName;
counterCreationData.CounterHelp = TotalObjectsReturnedPerSecPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.RateOfCountsPerSecond64;
counterCreationDataCollection.Add(counterCreationData);

// Total Get Misses/Sec
counterCreationData = new System.Diagnostics.CounterCreationData();
counterCreationData.CounterName = TotalGetMissesPerSecPerformanceCounterName;
counterCreationData.CounterHelp = TotalGetMissesPerSecPerformanceCounterDescription;
counterCreationData.CounterType = PerformanceCounterType.RateOfCountsPerSecond64;
counterCreationDataCollection.Add(counterCreationData);

// Create the category and pass the collection to it.
PerformanceCounterCategory.Create(WCFWebCachePerformanceCounterCategoryName,
WCFWebCachePerformanceCounterCategoryDescription,
PerformanceCounterCategoryType.MultiInstance,
counterCreationDataCollection);
}

// Get the current process
Process process = Process.GetCurrentProcess();
string instanceName = string.Format("{0} ({1})", process.ProcessName, process.Id);

// Create counters

// Total Object Count
totalObjectCountPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalObjectCountRequestsPerformanceCounterName,
instanceName,
false);
totalObjectCountPerformanceCounter.RawValue = 0;

// Total Client Requests
totalClientRequestsPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalClientRequestsPerformanceCounterName,
instanceName,
false);
totalClientRequestsPerformanceCounter.RawValue = 0;

// Total Objects Returned
totalObjectsReturnedPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalObjectsReturnedPerformanceCounterName,
instanceName,
false);
totalObjectsReturnedPerformanceCounter.RawValue = 0;

// Total Get Misses
totalGetMissesPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalGetMissesPerformanceCounterName,
instanceName,
false);
totalGetMissesPerformanceCounter.RawValue = 0;


// Total Client Requests/Sec
totalClientRequestsPerSecPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalClientRequestsPerSecPerformanceCounterName,
instanceName,
false);
totalClientRequestsPerSecPerformanceCounter.RawValue = 0;

// Total Objects Returned/Sec
totalObjectsReturnedPerSecPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalObjectsReturnedPerSecPerformanceCounterName,
instanceName,
false);
totalObjectsReturnedPerSecPerformanceCounter.RawValue = 0;

// Total Get Misses/Sec
totalGetMissesPerSecPerformanceCounter =
                                             new PerformanceCounter(WCFWebCachePerformanceCounterCategoryName,
TotalGetMissesPerSecPerformanceCounterName,
instanceName,
false);
totalGetMissesPerSecPerformanceCounter.RawValue = 0;

// If we found one set flag to true, this will be checked later on
// to determine whether to increment counters

performanceCountersExist = true;
}
catch (Exception ex)
{
// Failure usually indicates either permission problems or category does not exist,
// either way, we log the error and carry on without updating counters
ExceptionHelper.HandleException(ex);
performanceCountersExist = false;
}
}
#endregion

#region Public Static Methods
/// <summary>
/// Adds an object to the cache.
/// </summary>
/// <typeparam name="T">Type of item being added.</typeparam>
/// <param name="key">The unique value that is used to identify the object in the cache.</param>
/// <param name="value">The object saved to the cache cluster.</param>
/// <param name="timeout">The amount of time the object should reside in the cache before expiration.</param>
public static void Add<T>(string key, T value, TimeSpan timeout)
{
if (value == null)
{
return;
}

if (store[key] != null)
{
store.Remove(key);
}

store.Add(key,
value,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
timeout,
System.Web.Caching.CacheItemPriority.Default,
new System.Web.Caching.CacheItemRemovedCallback(OnRemoveCallBack));

if (performanceCountersExist)
{
totalObjectCountPerformanceCounter.Increment();
}
}

/// <summary>
/// Used to retrieve an item from the cache.
/// </summary>
/// <typeparam name="T">Type of item being added.</typeparam>
/// <param name="key">The unique value that is used to identify the object in the cache.</param>
/// <returns>An item of type T if found in the cache using the key provided, null otherwise.</returns>
public static T Get<T>(string key)
{
if (performanceCountersExist)
{
totalClientRequestsPerformanceCounter.Increment();
totalClientRequestsPerSecPerformanceCounter.Increment();
}

T cacheObject = (T)store.Get(key);

if (performanceCountersExist)
{
if (cacheObject != null)
{
totalObjectsReturnedPerformanceCounter.Increment();
totalObjectsReturnedPerSecPerformanceCounter.Increment();
}
else
{
totalGetMissesPerformanceCounter.Increment();
totalGetMissesPerSecPerformanceCounter.Increment();
}
}

return cacheObject;
}
#endregion

#region Private Static Methods
/// <summary>
/// Called when the item is being removed from the cache (due to expiration, for example),
/// used to decrement the performance counter which value returns the total amount of items in cache.

/// </summary>
/// <param name="key">The key that is removed from the cache.</param>
/// <param name="item">The Object item associated with the key removed from the cache.</param>
/// <param name="reason">The reason the item was removed from the cache,
/// as specified by the CacheItemRemovedReason enumeration.</param>

private static void OnRemoveCallBack(string key, object item, System.Web.Caching.CacheItemRemovedReason reason)
{
if (performanceCountersExist)
{
totalObjectCountPerformanceCounter.Decrement();
}
}
#endregion
}

 

The following table shows the code for the ClientCacheEndpointBehavior class that can be used to plug-in the caching channel on top of the channel stack created by a built-in binding like the BasicHttpBinding.

public class ClientCacheEndpointBehavior : IEndpointBehavior
{
#region Private Fields
private TimeSpan timeout;
private int maxBufferSize;
private string indexes;
private string keyCreationMethod;
private string cacheType;
private bool enabled;
private bool header;
private OperationElementCollection operations;
#endregion

#region Public Constructors
public ClientCacheEndpointBehavior(TimeSpan timeout,
int maxBufferSize,
string indexes,
string keyCreationMethod,
string cacheType,
bool enabled,
bool header,
OperationElementCollection operations)
{
this.timeout = timeout;
this.maxBufferSize = maxBufferSize;
this.indexes = indexes;
this.keyCreationMethod = keyCreationMethod;
this.cacheType = cacheType;
this.enabled = enabled;
this.header = header;
this.operations = operations;
}
#endregion

#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{

}

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
BindingElementCollection collection = endpoint.Binding.CreateBindingElements();
List<BindingElement> list = new List<BindingElement>();
list.Add(new ClientCacheBindingElement(this.timeout,
this.maxBufferSize,
this.indexes,
this.keyCreationMethod,
this.cacheType,
this.enabled,
this.header,
this.operations));
list.AddRange(collection);
CustomBinding customBinding = new CustomBinding(list);
customBinding.CloseTimeout = endpoint.Binding.CloseTimeout;
customBinding.Name = endpoint.Binding.Name;
customBinding.Namespace = endpoint.Binding.Namespace;
customBinding.OpenTimeout = endpoint.Binding.OpenTimeout;
customBinding.ReceiveTimeout = endpoint.Binding.ReceiveTimeout;
customBinding.SendTimeout = endpoint.Binding.SendTimeout;
endpoint.Binding = customBinding;
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{

}

public void Validate(ServiceEndpoint endpoint)
{
return;
}
#endregion
}

 

Finally, the following table shows the code of the ClientCacheRequestChannel class. Note: this class implements the IRequestChannel standard interface that is implemented by most of the WCF channels. However, different bindings may create different kinds of channel at runtime. In this case, you should create another class that implements the appropriate channel contract (e.g. IDuplexChannel).

public class ClientCacheRequestChannel<TChannel> : ClientCacheChannelBase<IRequestChannel>, IRequestChannel
{
    #region Private Constants
    private const string DefaultSettingsKey = "DefaultSettingsKey";
    private const string headerName = "source";
    private const string headerNamespace = "http://appfabric.cat.microsoft.com/10/samples/wcf/clientcaching";
    private const string cacheSource = "Cache";
    private const string serviceSource = "Service";
    #endregion

    #region Private Fields
    private TimeSpan timeout;
    private int maxBufferSize;
    private KeyCreationMethod keyCreationMethod;
    private CacheType cacheType;
    private bool enabled;
    private bool header;
    private List<int> indexList;
    private Dictionary<string, OperationSettings> operationDictionary;
    #endregion

    #region Public Constructors
    public ClientCacheRequestChannel(ClientCacheChannelFactory<TChannel> factory, 
                                        IRequestChannel innerChannel, 
                                        TimeSpan timeout, 
                                        int maxBufferSize,
                                        KeyCreationMethod keyCreationMethod,
                                        CacheType cacheType,
                                        bool enabled,
                                        bool header,
                                        List<int> indexList,
                                        Dictionary<string, OperationSettings> operationDictionary)
        : base(factory, innerChannel)
    {
        this.timeout = timeout;
        this.maxBufferSize = maxBufferSize;
        this.keyCreationMethod = keyCreationMethod;
        this.cacheType = cacheType;
        this.enabled = enabled;
        this.header = header;
        this.indexList = indexList;
        this.operationDictionary = operationDictionary;
        if (!this.operationDictionary.ContainsKey(DefaultSettingsKey))
        {
            this.operationDictionary.Add(DefaultSettingsKey, 
new OperationSettings(indexList,
keyCreationMethod,
cacheType,
timeout,
maxBufferSize,
enabled,
header)); } } #endregion #region IRequestChannel Members public IAsyncResult BeginRequest(Message message, AsyncCallback callback, object state) { return this.BeginRequest(message, this.DefaultSendTimeout, callback, state); } public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state) { Message reply = this.Request(message); return new TypedCompletedAsyncResult<Message>(reply, callback, state); } public Message EndRequest(IAsyncResult result) { TypedCompletedAsyncResult<Message> reply = (TypedCompletedAsyncResult<Message>)result; return reply.Data; } public System.ServiceModel.EndpointAddress RemoteAddress { get { return this.InnerChannel.RemoteAddress; } } public Message Request(Message request, TimeSpan timeout) { // This variable will contain the response to deliver back, from cache or inner channel Message response; // Store the cacheKey evaluated for request. // This will eventually be used to store the response in the cache // should the custom channel call the target service via its inner channel string cacheKey; // Retrieve operation settings from the dictionary OperationSettings operationSettings = GetOperationSettings(request.Headers.Action); if (operationSettings.Enabled) { // Try to get the response from the cache response = ReadFromCache(operationSettings, ref request, out cacheKey); if (response == null) { response = this.InnerChannel.Request(request); if (operationSettings.Header) { response.Headers.Add(MessageHeader.CreateHeader(headerName, headerNamespace, serviceSource)); } // The message is not cached if it's a fault or empty. if (response != null && !response.IsFault && !response.IsEmpty) { WriteToCache(operationSettings, cacheKey, ref response); } } } else { response = this.InnerChannel.Request(request); if (operationSettings.Header) { response.Headers.Add(MessageHeader.CreateHeader(headerName, headerNamespace, serviceSource)); } } return response; } public Message Request(Message message) { return this.Request(message, this.DefaultSendTimeout); } public Uri Via { get { return this.InnerChannel.Via; } } #endregion #region Private Methods /// <summary> /// Returns an instance of the OperationInfo class which contains settings for the call. /// </summary> /// <param name="action">The WS-Addressing action of the request message.</param> /// <returns></returns> private OperationSettings GetOperationSettings(string action) { if (operationDictionary.ContainsKey(action)) { return operationDictionary[action]; } else { return operationDictionary[DefaultSettingsKey]; } } /// <summary> /// Attempts to find a response for the request provided in the cache /// Returns a response if found, or null otherwise. /// Also provides the key used for the cache should the caller want to update the cache for this request /// The request message will be re-created if the response was not found in the cache
/// so that the caller can send it to the inner channel
/// </summary> /// <param name="request">The operation settings.</param> /// <param name="request">The request for which a response is needed, used to extract the cache key
/// to use.</param>
/// <param name="cacheKey">The cache key evaluated for the request.</param> /// <returns>A response message if found in the cache, null otherwise.</returns> private Message ReadFromCache(OperationSettings operationSettings, ref Message request, out string cacheKey) { cacheKey = null; MessageBuffer requestBuffer = null; try { if (request != null) { // Evaluate the cache key for the request, also returns the buffer of
// the request message (as it is being read by the function)
requestBuffer = request.CreateBufferedCopy(operationSettings.MaxBufferSize); cacheKey = KeyCreationHelper.GenerateKey(requestBuffer.CreateMessage(), operationSettings); Message cachedResponse = null; // Attempt to get a response from the cache as a message buffer MessageCacheItem messageCacheItem = null; switch (operationSettings.CacheType) { case CacheType.MemoryCache: messageCacheItem = MemoryCache.Cache.Get<MessageCacheItem>(cacheKey); break; case CacheType.WebCache: messageCacheItem = WebCache.Cache.Get<MessageCacheItem>(cacheKey); break; default: messageCacheItem = AppFabricCache.Cache.Get<MessageCacheItem>(cacheKey); break; } if (messageCacheItem != null) { MemoryStream messageCopy = new MemoryStream(messageCacheItem.Buffer); messageCopy.Seek(0, SeekOrigin.Begin); cachedResponse = Message.CreateMessage(request.Version, messageCacheItem.Action, new XmlTextReader(messageCopy)); if (operationSettings.Header) { cachedResponse.Headers.Add(MessageHeader.CreateHeader(headerName,
headerNamespace,
cacheSource)); } } if (cachedResponse == null) { // If we haven't found a cached response the request will need to be
// sent through the underlying channel.
// as we've read it we have to create a new message from the buffer. request = requestBuffer.CreateMessage(); return null; } else { // Return the cached response return cachedResponse; } } } finally { // Close the request buffer if (requestBuffer != null) { requestBuffer.Close(); } } return null; } /// <summary> /// Writes a message in the cache. /// </summary> /// <param name="request">The operation settings.</param> /// <param name="cacheKey">The unique key that is used to identify the object in the cache.</param> /// <param name="response">The message to add to the cache.</param> private void WriteToCache(OperationSettings operationSettings, string cacheKey, ref Message response) { MessageBuffer responseBuffer = null; try { // Create a buffered copy of the response message responseBuffer = response.CreateBufferedCopy(maxBufferSize); // Store the buffer in the cache using the key provided
// (which was evaluated from the corresponding request)
// Create a message cache item // Note: neither the Message class nor the MessageBuffer class are serializable with // DataContractSerializer. Therefore, I chose to create the MessageCacheItem class // to save the data necessary to rebuild the response message. MemoryStream stream = new MemoryStream(maxBufferSize); Message messageCopy = responseBuffer.CreateMessage(); XmlWriterSettings settings = new XmlWriterSettings(); settings.OmitXmlDeclaration = true; using (XmlWriter xmlWriter = XmlWriter.Create(stream, settings)) { using (XmlDictionaryWriter xmlDictionaryWriter =
XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter)) { messageCopy.WriteBodyContents(xmlDictionaryWriter); stream.Seek(0, SeekOrigin.Begin); } } switch (operationSettings.CacheType) { case CacheType.MemoryCache: MemoryCache.Cache.Add<MessageCacheItem>(cacheKey, new MessageCacheItem(response.Headers.Action, stream.ToArray()), timeout); break; case CacheType.WebCache: WebCache.Cache.Add<MessageCacheItem>(cacheKey, new MessageCacheItem(response.Headers.Action, stream.ToArray()), timeout); break; default: AppFabricCache.Cache.Add<MessageCacheItem>(cacheKey, new MessageCacheItem(response.Headers.Action, stream.ToArray()), timeout); break; } // Create a message out of the buffer and returns as the response response = responseBuffer.CreateMessage(); } finally { // Close the response buffer if (responseBuffer != null) { responseBuffer.Close(); } } } #endregion }

 

Conclusions

The caching channel shown in this article can be used to extend existing applications that use WCF to invoke one or multiple back-end services and inject caching capabilities. The solution presented in this article can be further extended to implement new channel types other than the IRequestChannel and new key creation algorithms. The source code that accompanies the article can be downloaded here. As always, any feedback is more than welcome!

References

For more information on the AppFabric Caching, see the following articles:

  • “Windows Server AppFabric Cache: A detailed performance & scalability datasheet” whitepaper on Grid Dynamics.
  • “Windows Server AppFabric Caching Logical Architecture Diagram” topic on MSDN.
  • “Windows Server AppFabric Caching Physical Architecture Diagram” topic on MSDN.
  • “Windows Server AppFabric Caching Deployment and Management Guide” guide on MSDN.
  • “Lead Hosts and Cluster Management (Windows Server AppFabric Caching)” topic on MSDN.
  • “High Availability (Windows Server AppFabric Caching)” topic on MSDN.
  • “Security Model (Windows Server AppFabric Caching)” topic on MSDN.
  • “Using Windows PowerShell to Manage Windows Server AppFabric Caching Features” topic on MSDN.
  • “Expiration and Eviction (Windows Server AppFabric Caching)” topic on MSDN.
  • “Concurrency Models (Windows Server AppFabric Caching)” topic on MSDN.
  • “Build Better Data-Driven Apps With Distributed Caching” article on the MSDN Magazine.
  • “AppFabric Cache - Peeking into client & server WCF communication” article on the AppFabric CAT blog.
  • “A Configurable AppFabric Cache Attribute For Your WCF Services” article on the AppFabric CAT blog.
  • “Guidance on running AppFabric Cache in a Virtual Machine (VM)” article on the AppFabric CAT blog.
  • “Tagging Objects in the AppFabric Cache” article on Stephen Kaufman's WebLog.
  • “Pre-Populate the AppFabric Cache” article on Stephen Kaufman's WebLog.
  • Hi, I tried the sample code AS-IS and the cache seems to be not working. As per your test project, both Assert.AreEqual statmements retrieve data from WCF service instead of second call retrieve the data from cache. Am i missing something?

  • Hi Vinoth, which test method did you try out? Some of the test methods (e.g. TestAction) assume that you perform the following steps:

    - install and configure Windows Server AppFabric Caching on the local machine.

    - You turn off security at the cache cluster level to match security settings contained in the configuration file of the test project. In order to do that you can use the following PowerShell cmdlet: Set-CacheClusterSecurity -SecurityMode None -ProtectionLevel None

    - You create the WCFClientCache cache using the script contained in the zip file: New-Cache -CacheName WCFClientCache -NotificationsEnabled true -Eviction LRU -Expirable true -TimeToLive 60.

    I hope this help to solve your problem! Don't hesitate to contact me back!

    Ciao,

    Paolo

  • Hi,

    How can I use this project with Custom TCP Binding. Custom Tcp Binding use default IDuplexSessionChannel. I tried to change TransferMode to Streamed, So It use IRequesthannel but It throw exception. How can I fix this problem.

    Do you have any idea?

    Thank you

    Have nice day

  • Hi Sinan,

    I'm quite busy in this period, but it would be great if you could send me a repro. As a I said in the article, and as you highlighted in your comment, my component actually supports the IRequestChannel interface. Hence, you should extend my library to include a channel that implements the IDuplexSessionChannel or extend the existing channel to support this interface. I didn't try to use the TransferMode=Streamed, so I can't answer your question. :( I'll do my best to make a test, but in this period I'm very busy, so expect a delayed answer!

    Ciao,

    Paolo

  • Hi Paolo,

    I'm waiting for yor answer.

    Thank you for all

  • Hi Sinan,

    maybe you didn't notice my previous reply. ;) As I said, my component actually supports only the IRequestChannel interface. Hence, you should extend my library to include a channel that implements the IDuplexSessionChannel or extend the existing channel to support this interface. I didn't try to use the TransferMode=Streamed, so I can't answer your question. :( It would be great if you could send me a repro of the issue you are experiencing!

    Ciao,

    paolo

  • Hi Paolo

    My English is not very good. So that I couldn't notice your reply. When I implement IDuplexChanel interface, I will send implementation to you.

    Have nice day

  • Hi Paolo,

    first of all: really great post! I believe WCF caching is really something that is needed in most real-life-projects. Were you able to implement a IDuplexSessionChannel-version already?

  • Hi mg.net,

    unfortunately I didn't time to implement a channel that implements the IDuplexSessionChannel interface. In the meantime, I changed focus to other technologies, so I won't create such a component. Should you decide to create your own version, please leave a comment here along with the link to your code! ;)

    Ciao,

    Paolo

  • I know this is an old post, but I was wondering if one should not use an HTTP Module instead of a custom channel. The module can cache responses and send that back to the consumer.

  • Of course! The request needs to go through many levels, so using an HTTP Module can be an altenative solution. The problem with this solution is to recognize the right target service and method. This is automatic when using a client channel. On the other end, the HTTP Module could be implemented in such a way that it could be reused to cache responses in other contexts other than WCF calls. By the way HTTP Module exist for years, even when I created my component, which is not so old as you said. ;) The reason why I implemented it as custom channel it's more WCF specific and I have more control. Having said that, if you prefer to implement and HTTP Module, you can still reuse part o my code. Good luck! ;)

  • OK, so I finally (after 2 years on the blog shelf) published my post on using the WCF Client Cache for SharePoint

    blog.kloud.com.au/.../sharepoint-web-service-caching-using-wcf-custom-channel

    Incidentally its the move to SharePoint Online and the new SharePoint App model that motivated the latest implementation.

  • Thanks Peter for sharing your article on my blog. My compliments, very well done! :)

    Ciao

    Paolo

Page 1 of 1 (13 items)
Leave a Comment
  • Please add 5 and 2 and type the answer here:
  • Post
Search Blogs