Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to Create WCF Caching Operation Behavior

0.00/5 (No votes)
7 Nov 2014 1  
WCF Caching

The Problem

WCF is a very important framework in the modern .NET applications and it is so very rich of features and aspects.

Caching is a very important cross cutting aspect that is very important and needed in most kind of applications specially the LOB kind.

Although traditional web methods did support caching on the fly by setting the CacheDuration value to the needed duration as in [WebMethod(CacheDuration=10)], yet the rich WCF service does not support that in a direct straight way to say the least.

The Solution

The Simple Way

One way to do that is to use AspNetCacheProfileAttribute, and set the caching period as a parameter, the nice thing about this is that it is so simple and it is so configurable since all caching parameters can be controlled by configuration and that is a great advantage.

The disadvantage or the limitation of this attribute is that it works only within these conditions:

  1. Your WCF service works in an AspNet Compatible mode.
  2. It should have WebGet attribute
  3. It uses "webHttpBinding" as a binding.

Not all services meet these conditions and there are many situations where you are NOT able or don’t want to do that to your services and make them RestFul.

Mind you that this situation will force you to consume your service only by GET method and will forbid you from using any other method like (POST, DELETE, etc.) which is somehow fine since you are getting data to be cached but SOAP usually uses the POST method to make a call.

Suppose I have this service:

    [ServiceContract]
    public interface ITestService
    {
        [OperationContract]
        string TimeConsumingMethod();
    }

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

    public class TestService : ITestService
    {
        [AspNetCacheProfile("GetReferenceTableCollectionProfile"), 
        WebGet(UriTemplate = "TimeConsumingMethod", 
        RequestFormat= WebMessageFormat.Json)]
        public string TimeConsumingMethod()
        {
            System.Threading.Thread.Sleep(10000);
            return "AWESOME";
        }
    }

The configuration has this section:

  <system.web>

    <caching>
        <outputCache enableOutputCache="true"></outputCache>
        <outputCacheSettings >
          <outputCacheProfiles>
            <add name="GetReferenceTableCollectionProfile" 
            duration ="6000" enabled ="true" varyByParam ="request" />
          </outputCacheProfiles>
        </outputCacheSettings>
      </caching>
     </system.web>

And also this section for IIS host WCF:

<system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name ="WebHttp" >
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <services>
      <service name="TestService.TestService" >
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost/TestService/TestService.svc"/>
          </baseAddresses>
        </host>
        <endpoint address="" binding="webHttpBinding" 
        name="RestFul" contract="TestService.ITestService" 
        behaviorConfiguration="WebHttp"></endpoint>
        <endpoint address="" binding="basicHttpBinding" 
        name="Default" contract="TestService.ITestService"></endpoint>
      </service>
    </services>
    <protocolMapping>
        <add binding="basicHttpsBinding" scheme="https" />
    </protocolMapping>   
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" 
    multipleSiteBindingsEnabled="true" />
  </system.serviceModel>

The client would look something like the following after referencing the WCF as a web reference:

var factory = new ChannelFactory<ITestService>("Default");
var proxy = factory.CreateChannel();
for (int i = 0; i < 20; )
{
    proxy.TimeConsumingMethod();
    Console.WriteLine("{0}: was called for the {1} time", "TimeConsumingMethod", ++i);
}

Console.ReadLine();

Try the code above and then try the code below and tell me what the difference is:

var factory = new ChannelFactory<ITestService>("RestFul");
factory.Endpoint.EndpointBehaviors.Add(new WebHttpBehavior());
var proxy = new Proxy.TestServiceClient("RestFul");
for (int i = 0; i < 20; )
{
    proxy.TimeConsumingMethod();
    Console.WriteLine("{0}: was called for the {1} time", "TimeConsumingMethod", ++i);
}
 
for (int i = 0; i < 10; i++)
{
    CallGetMethod();
}
 
Console.ReadLine();

The Right way

To create a WcfOperation behavior for caching that uses a caching mechanism, in my sample I used System.RuntimeCaching.

In order to make the solution more practical, let’s also create a caching invalidation mechanism that will do the invalidation of the expiry.

Although this article was supposed to talk all about these mechanisms, it looks like time is going by so fast.

I will leave you with the code and the sample, let me know if you have questions.

Let's start with this client application:

string address="net.pipe://localhost/whatever";
            using (var host = new ServiceHost(typeof(TestService), new Uri(address)))
            {
                host.Open();
                var proxy = ChannelFactory<ITestService>.
                CreateChannel(new NetNamedPipeBinding(), new EndpointAddress(new Uri(address)));
                for (int i = 0; i < 5; i++)
                {
                   Console.WriteLine(  proxy.TimeConsumingMethod());
                }
                Console.WriteLine("Hit Enter to continue");
                Console.ReadLine();
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine(proxy.TimeConsumingMethod());
                    proxy.Update();
                }
                host.Close();
            }

We see that the first loop runs fast because caching was enabled and it was a test for caching, whilst the latter loop is running slow because the cache is being invalidated by calling the method update.

How did that happen?

Let’s have a look at the service and its implementation:

[ServiceContract]
   interface ITestService
   {
       [OperationContract]
       string TimeConsumingMethod();
       [OperationContract]
       void Update();
   }

  [ServiceBehavior(IncludeExceptionDetailInFaults=true)]
  class TestService : ITestService
  {
      [CachingBehavior(0.3, null, new[] { typeof(FirstExpirer) })]
      public string TimeConsumingMethod()
      {
          int waitTime = 2000;
          System.Threading.Thread.Sleep(waitTime);
          return string.Format("Done waiting for {0} milliseconds", waitTime);
      }
 
      [ExpiryBehavior(new[] { typeof(FirstExpirer) })]
      public void Update()
      {
         //The update logic 
      }
  }

How It Works

Simply, the behaviors will create and use OperationInvokers.

Because the two behaviors I have created are similar in function and so are the invokers, I created a base class common for each.

Also, I created base classes for invalidator and keyGenerator.

The role or responsibility of the key generator is to generate a unique caching id for each parameter that is passed the operation, i.e., all the parameters that are passed to the operation together along with the operation name and contract are responsible to generate a unique key to that operation so that once it is called with the same values as parameters, they will reach the same caching key, otherwise they will compose a new unique one.

Invalidators are a bit tricky, but the concept is simple.

Suppose I have two operations, one to get the data and the other is to update it.

I need to create one single type that links these two methods together and adds change monitor to the cachitem, once the method that updates is called, the cached item should be removed and that is done using the invalidator.

We need some kind of manager to manage these invalidators and clear memory of them when not needed.

I would also recommend using interfaces instead of base classes when it comes to invalidator and key generator, especially when you need to build a general purpose solution that is reliable and extendable.

Let's start with the base classes which usually are basics:

   public abstract class OperationBehaviorAttributebase : Attribute, IOperationBehavior
    {
        public virtual  void AddBindingParameters(OperationDescription operationDescription, 
        System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
            
        }

        public virtual void ApplyClientBehavior(OperationDescription operationDescription, 
        System.ServiceModel.Dispatcher.ClientOperation clientOperation)
        {
           
        }

        public virtual void ApplyDispatchBehavior(OperationDescription operationDescription, 
        System.ServiceModel.Dispatcher.DispatchOperation dispatchOperation)
        {
           
        }

        public virtual void Validate(OperationDescription operationDescription)
        {
           
        }
    }

public abstract class OperationInvokerbase : IOperationInvoker
    {
        private IOperationInvoker _originalInvoker;
        private string _operationName;
        protected Type[] _WcfMonitorTypes;
        private Type[] _keyGeneratorTypes;
        public OperationInvokerbase(IOperationInvoker invoker, 
        string operationName, Type[] keyGeneratorTypes, Type[] WcfMonitorTypes)
        {
            _originalInvoker = invoker;
            _operationName = operationName;
            _WcfMonitorTypes = WcfMonitorTypes;
            _keyGeneratorTypes = keyGeneratorTypes;
        }
        public virtual object[] AllocateInputs()
        {
            return _originalInvoker.AllocateInputs();
        }

        public virtual object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            return this._originalInvoker.Invoke(instance, inputs, out outputs);
        }

        public virtual IAsyncResult InvokeBegin(object instance, 
        object[] inputs, AsyncCallback callback, object state)
        {
            return this._originalInvoker.InvokeBegin(instance, inputs, callback, state);
        }

        public virtual object InvokeEnd
        (object instance, out object[] outputs, IAsyncResult result)
        {
            return this._originalInvoker.InvokeEnd(instance, out outputs, result);
        }

        public virtual bool IsSynchronous
        {
            get { return this._originalInvoker.IsSynchronous; }
        }

        protected string GenerateKey(object instance, object[] inputs)
        {

            var sb = new StringBuilder();
            sb.Append(instance.GetType().FullName);
            sb.Append(".");
            sb.Append(_operationName);
            sb.Append(".");
            foreach (var parameter in inputs)
            {
                if (parameter is ValueType || parameter is string)
                {
                    sb.Append(parameter);
                    sb.Append(",");
                }
                else
                {
                    var parameterType = parameter.GetType();
                    Type keyGeneratorType = _keyGeneratorTypes.SingleOrDefault
                    (x => !x.IsGenericType ? x.BaseType.GetGenericArguments()[0] 
                    == parameterType : x.GetGenericArguments()[0] == parameterType);
                    if (null == keyGeneratorType)
                        throw new InvalidOperationException(string.Format
                        ("There is no Key Generator provided for type {0}", parameterType.FullName));

                    var keyGenerator = Activator.CreateInstance(keyGeneratorType);
                    var methodInfo = keyGeneratorType.GetMethod("GetKey");
                    var key = methodInfo.Invoke(keyGenerator, new[] { parameter });
                    sb.Append(key);
                }
            }

            return sb.ToString();
        }

        public ObjectCache CacheProvider { get { return MemoryCache.Default; } }
    }

  public abstract class WcfMonitorbase : ChangeMonitor
    {
        private string uniqueId;
        public abstract string UniqueKey { get; }

        protected override void Dispose(bool disposing)
        {

        }

        public override string UniqueId
        {
            get
            {
                if (string.IsNullOrEmpty(uniqueId)) uniqueId = Guid.NewGuid().ToString();
                return uniqueId;
            }
        }

        public WcfMonitorbase()
        {
            InitializationComplete();
        }

        public void Expire()
        {
           //remove from the collection
            var dependency = (WcfMonitorbase)DependencyManager.Remove(this.UniqueKey);
            //expire the cache entry 
           if (null != dependency)
           {
               dependency.OnChanged(null);
               dependency.Dispose();

               if (null != dependency.Expired)
                   dependency.Expired(dependency, new EventArgs());
           }
        }

        internal delegate void ExpiredHandler( object sender, EventArgs e);

        internal event ExpiredHandler Expired; 
    }

public abstract class KeyGeneratorbase<T>
    {
        public abstract string GetKey(T parameter);      
    }

Now the classes' implementations:

public class CachingBehavior : OperationBehaviorAttributebase
    {

        double _cachingInMinutes;
        private Type[] _WcfMonitorTypes;
        private Type[] _keyGeneratorTypes;
        public CachingBehavior(double cachingInMinutes, 
        Type[] keyGeneratorTypes, Type[] WcfMonitorTypes)
        {
            _cachingInMinutes= cachingInMinutes;
            _WcfMonitorTypes = WcfMonitorTypes;
           _keyGeneratorTypes= keyGeneratorTypes;
        }
        public override void ApplyDispatchBehavior
        (OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            dispatchOperation.Invoker = new CachingOperationInvoker
            (dispatchOperation.Invoker,_cachingInMinutes, 
            operationDescription.Name,_keyGeneratorTypes  ,_WcfMonitorTypes);
        }

    public class ExpiryBehavior : OperationBehaviorAttributebase 
    {
        private Type[] _WcfMonitorTypes;

        public ExpiryBehavior(Type[] WcfMonitorTypes)
        {
            _WcfMonitorTypes = WcfMonitorTypes;
        }
        public  override void ApplyDispatchBehavior
        (OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            dispatchOperation.Invoker = new ExpiryOperationInvoker
            (dispatchOperation.Invoker, operationDescription.Name, null ,_WcfMonitorTypes);
        }      
    }

class ExpiryOperationInvoker : OperationInvokerbase
    {
        
         private string _operationName;

         public ExpiryOperationInvoker(IOperationInvoker invoker, 
         string operationName, Type[] keyGeneratorTypes, Type[] WcfMonitorTypes)
             : base(invoker, operationName, keyGeneratorTypes, WcfMonitorTypes)
        {
            _operationName = operationName;
        }

        public override object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            var key = GenerateKey(instance, inputs);
            if (CacheProvider.Contains(key)) CacheProvider.Remove(key);

            if (null != _WcfMonitorTypes)
            {

                foreach (var monitorType in _WcfMonitorTypes)
                {
                    var monitor = (WcfMonitorbase)Activator.CreateInstance(monitorType);
                    monitor.Expire();
                    DependencyManager.Remove(monitor.UniqueKey);
                  
                }
            }

            return base.Invoke(instance, inputs, out outputs);
        }
    }

class CachingOperationInvoker : OperationInvokerbase
    {
        private double _cachingInMinutes;
        private string _operationName;
        private static ConcurrentDictionary<WcfMonitorbase, List<string>> _monitors;

        public CachingOperationInvoker(IOperationInvoker invoker, 
        double cachingInMinutes, string operationName, Type[] keyGeneratorTypes, Type[] WcfMonitorTypes)
            : base(invoker, operationName, keyGeneratorTypes, WcfMonitorTypes)
        {
            _cachingInMinutes = cachingInMinutes;
            _operationName = operationName;
            _monitors = new ConcurrentDictionary<WcfMonitorbase, List<string>>();
        }

        public override object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            string key = GenerateKey(instance, inputs);

            if (CacheProvider.Contains(key))
            {
                var allResults = (object[])CacheProvider[key];
                var result = allResults[0];
                outputs = (object[])allResults[1];
                return result;
            }
            else
            {
                var result = base.Invoke(instance, inputs, out outputs);
                var policy = GetPolicy();
                CacheProvider.Add(key, new object[] { result, outputs }, policy);

                if(null!= _WcfMonitorTypes)
                {
                 
                    foreach (var monitorType in _WcfMonitorTypes)
                    {
                        var monitor = (WcfMonitorbase)Activator.CreateInstance(monitorType);
                        monitor.Expired += monitor_Expired;
                        DependencyManager.GetOrAdd( monitor);
                        List<string> associatedKeys= new List<string> ();
                        _monitors.GetOrAdd(monitor, associatedKeys);
                        associatedKeys.Add(key);
                    }                   
                }
                else
                {
                    //No Invalidators 
                }
               
                return result;
            }
        }

        void monitor_Expired(object sender, EventArgs e)
        {
            List<string> associatedKeys = new List<string>();
            var mon = DependencyManager.GetOrAdd((WcfMonitorbase)sender);
            _monitors.TryGetValue((WcfMonitorbase)mon, out associatedKeys);
            if(null!=associatedKeys)
            {
                foreach (var key in associatedKeys)
                {
                    CacheProvider.Remove(key);
                }
            }
        }

        private CacheItemPolicy GetPolicy()
        {
            CacheItemPolicy cacheItemPolicy = new CacheItemPolicy();
            cacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddMinutes(_cachingInMinutes);
            cacheItemPolicy.RemovedCallback += OnItemRemoved;
            return cacheItemPolicy;
        }

        private void OnItemRemoved(CacheEntryRemovedArguments arguments)
        {
            switch (arguments.RemovedReason)
            {
                case CacheEntryRemovedReason.CacheSpecificEviction:
                    break;
                case CacheEntryRemovedReason.ChangeMonitorChanged:
                    break;
                case CacheEntryRemovedReason.Evicted:
                    break;
                case CacheEntryRemovedReason.Expired:
                    break;
                case CacheEntryRemovedReason.Removed:
                    break;
                default:
                    break;
            }
        }
    }

    public  sealed class DependencyManager
    {
        private static ConcurrentDictionary<string, 
        WcfMonitorbase> _allDependencies = new ConcurrentDictionary<string, WcfMonitorbase>();

        public static WcfMonitorbase GetOrAdd(WcfMonitorbase depedency)
        {

            if (!_allDependencies.ContainsKey(depedency.UniqueKey))
                return _allDependencies.GetOrAdd(depedency.UniqueKey, depedency);
            return depedency;
        }

        public static WcfMonitorbase Remove(WcfMonitorbase depedency)
        {
            return Remove(depedency.UniqueKey);
        }
        public static WcfMonitorbase Remove(string depedencyKey)
        {
            WcfMonitorbase depedency = null;
            _allDependencies.TryRemove(depedencyKey, out depedency);
            return depedency;
        }

        public static int Count { get { return _allDependencies.Count; } }
    }

Results

Note

I apologize for any typos or bugs you may find because this was the fastest article I have ever written.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here