Coding Sanity

.NET development and technology thoughts
posts - 51, comments - 30, trackbacks - 0

Cache Headers in WCF REST

I wanted to add some HTTP cache headers to my outgoing WCF service responses. We have some static data, which never changes intraday, and it seemed silly to keep invoking the service again and again. I could have just put some caching on the code that’s consuming the data, but I wanted the service to be able to indicate the caching that should be used for the data it returns, since it’s the service that controls the item’s lifetime.

So, to start I just wanted to add support for indicating that the item returned expired at midnight. I created the following attribute:

   1:  public enum CacheAge
   2:  {
   3:      None,
   4:      ToMidnight,
   5:  }
   6:  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
   7:  public class CacheControlAttribute : Attribute, IOperationBehavior
   8:  {
   9:      public CacheAge MaxAge { get; set; }
  10:   
  11:      void IOperationBehavior.AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
  12:      {
  13:      }
  14:   
  15:      void IOperationBehavior.ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  16:      {
  17:      }
  18:   
  19:      void IOperationBehavior.ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  20:      {
  21:          dispatchOperation.Invoker = new CacheInvoker(operationDescription, dispatchOperation.Invoker, this);
  22:      }
  23:   
  24:      void IOperationBehavior.Validate(OperationDescription operationDescription)
  25:      {
  26:      }
  27:  }

This attribute would be applied to your service contract like this:

   1:  [ServiceContract, XmlSerializerFormat]
   2:  public interface ICustomerService
   3:  {
   4:      [OperationContract, WebGet(UriTemplate = "/{code}")]
   5:      [CacheControl(MaxAge = CacheAge.ToMidnight)]
   6:      Customer GetCustomer(string code);
   7:  }

So what this is saying is that when the call to the GetCustomer method can be cached until midnight.

The heavy lifting is done inside the CacheInvoker class you see a reference to in the first code snippet:

   1:  public class CacheInvoker : IOperationInvoker
   2:  {
   3:      private IOperationInvoker _invoker;
   4:      private CacheControlAttribute _cacheSettings;
   5:   
   6:      public CacheInvoker(OperationDescription operationDescription, IOperationInvoker invoker, CacheControlAttribute cacheSettings)
   7:      {
   8:          _invoker = invoker;
   9:          _cacheSettings = cacheSettings;
  10:      }
  11:   
  12:      private void AddCacheControl(object result)
  13:      {
  14:          OutgoingWebResponseContext outResponse = WebOperationContext.Current.OutgoingResponse;
  15:   
  16:          if (_cacheSettings.MaxAge == CacheAge.ToMidnight)
  17:          {
  18:              var expires = DateTime.Today.AddDays(1);
  19:              outResponse.Headers.Add("Cache-Control", "max-age=" + ((int)expires.Subtract(DateTime.Now).TotalSeconds).ToString());
  20:              outResponse.Headers.Add("Expires", expires.ToString("R"));
  21:          }
  22:      }
  23:   
  24:      object[] IOperationInvoker.AllocateInputs()
  25:      {
  26:          return _invoker.AllocateInputs();
  27:      }
  28:   
  29:      object IOperationInvoker.Invoke(object instance, object[] inputs, out object[] outputs)
  30:      {
  31:          var ret = _invoker.Invoke(instance, inputs, out outputs);
  32:          if (ret != null)
  33:              AddCacheControl(ret);
  34:          return ret;
  35:      }
  36:   
  37:      IAsyncResult IOperationInvoker.InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
  38:      {
  39:          return _invoker.InvokeBegin(instance, inputs, callback, state);
  40:      }
  41:   
  42:      object IOperationInvoker.InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
  43:      {
  44:          var ret = _invoker.InvokeEnd(instance, out outputs, result);
  45:          if (ret != null)
  46:              AddCacheControl(ret);
  47:          return ret;
  48:      }
  49:   
  50:      bool IOperationInvoker.IsSynchronous
  51:      {
  52:          get { return _invoker.IsSynchronous; }
  53:      }
  54:  }

Now, I’ve kept this relatively simple for demonstration purposes, but in my project I’ve also added support for ETags and the Last-Modified header. How it works is by inspecting the returned type looking for properties with appropriate attributes, and using those to output the relevant headers.

The beauty of this system is that it hooks directly into the HTTP infrastructure; which WCF respects. You don’t need to create your own complex caching scheme - you can use the one supplied for you in a few easy steps.

Print | posted on Monday, September 06, 2010 5:03 PM | Filed Under [ Code Performance ]

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 7 and 1 and type the answer here:

Powered by:
Powered By Subtext Powered By ASP.NET