« Home | We're having a baby! » | Mexico Vacation April 2006 » | New Position! » | NMock Presentation » | How to replace MSN Toolbar default search » | Favorite Resharper Keyboard Shortcuts » | VSS Tips » | NUnit VS2003 Template » | Installing Vista on Virtual PC » | PDC 2005 Highlights »

My First Soap Extension

I've finally got a chance to write a Soap Extension. This one allows you to have caching for a web service method but the cache duration can be specified in the web.config file. The .NET "WebMethod" attribute supports caching but requires a constant value for the cache duration.

Here's the code for the extension:


   1:  using System;
   2:  using System.Configuration;
   3:  using System.Diagnostics;
   4:  using System.IO;
   5:  using System.Web;
   6:  using System.Web.Services.Protocols;
   7:   
   8:  namespace CacheExtensionProj
   9:  {
  10:      [AttributeUsage(AttributeTargets.Method)]
  11:      public class CacheExtensionAttribute : SoapExtensionAttribute 
  12:      {
  13:          private string cacheDurationConfigKey;
  14:          private int priority;
  15:   
  16:          public override Type ExtensionType 
  17:          {
  18:              get { return typeof(CacheExtension); }
  19:          }
  20:   
  21:          public override int Priority 
  22:          {
  23:              get { return priority; }
  24:              set { priority = value; }
  25:          }    
  26:   
  27:          public string CacheDurationConfigKey
  28:          {
  29:              get { return cacheDurationConfigKey;}
  30:              set { cacheDurationConfigKey = value;}
  31:          }
  32:      }
  33:   
  34:      public class CacheExtension : SoapExtension 
  35:      {
  36:          Stream originalStream;
  37:          Stream newStream;
  38:          string cacheKey = null;
  39:          string cacheDurationConfigKey;
  40:          int cacheDuration = 0;
  41:   
  42:          public override object GetInitializer(LogicalMethodInfo methodInfo,
  43:              SoapExtensionAttribute attribute) 
  44:          {
  45:              //Cast attribute and return attribute value
  46:              CacheExtensionAttribute cea = (CacheExtensionAttribute)attribute;
  47:              return cea.CacheDurationConfigKey;
  48:          }
  49:   
  50:          public override object GetInitializer(Type serviceType)
  51:          {
  52:              return null;
  53:          }
  54:   
  55:          public override void Initialize(object initializer) 
  56:          {
  57:              cacheDurationConfigKey = (string)initializer;            
  58:              if (cacheDurationConfigKey == null 
  59:                   cacheDurationConfigKey == string.Empty)
  60:              {
  61:                  cacheDurationConfigKey = "DefaultCacheDuration";
  62:              }
  63:   
  64:              cacheDuration = GetCacheDuration(cacheDurationConfigKey);        
  65:          }
  66:   
  67:          public override void ProcessMessage(SoapMessage message) 
  68:          {
  69:              switch (message.Stage) 
  70:              {
  71:                  case SoapMessageStage.BeforeSerialize:
  72:                      break;
  73:   
  74:                  case SoapMessageStage.AfterSerialize:                
  75:                      newStream.Position = 0;  //need to reset stream 
  76:                      CopyStream(newStream, originalStream);
  77:                      
  78:                      SaveStreamToCache(cacheKey, newStream);                    
  79:                      break;
  80:   
  81:                  case SoapMessageStage.BeforeDeserialize:
  82:                      //Cache key is the entire request xml string
  83:                      cacheKey = GenerateCacheKeyFromStream(originalStream);
  84:                                          
  85:                      Stream stream = GetStreamFromCache(cacheKey);
  86:                      if (stream != null)
  87:                      {
  88:                          Trace.WriteLine("Returning Cached Version");
  89:                          HttpContext.Current.Response.ContentType = 
  90:                                  "text/xml";
  91:                          CopyStream(stream, 
  92:                              HttpContext.Current.Response.OutputStream);
  93:                          HttpContext.Current.Response.Flush();
  94:                          HttpContext.Current.Response.End(); 
  95:                      }
  96:                      else
  97:                      {
  98:                          CopyStream(originalStream, newStream);
  99:                          newStream.Position = 0;  //need to reset stream
 100:                      }
 101:                      break;
 102:   
 103:                  case SoapMessageStage.AfterDeserialize:
 104:                      break;
 105:   
 106:                  default:
 107:                      throw new Exception("invalid stage");
 108:              }            
 109:          }
 110:   
 111:          public override Stream ChainStream( Stream stream )
 112:          {
 113:              originalStream = stream;
 114:              newStream = new MemoryStream();
 115:              return newStream;
 116:          }
 117:   
 118:          private void CopyStream(Stream from, Stream to) 
 119:          {
 120:              TextReader reader = new StreamReader(from);
 121:              TextWriter writer = new StreamWriter(to);
 122:              writer.WriteLine(reader.ReadToEnd());
 123:              writer.Flush();
 124:          }
 125:   
 126:          private string GenerateCacheKeyFromStream(Stream stream)
 127:          {
 128:              string cacheKey = new StreamReader(stream).ReadToEnd();
 129:              stream.Position = 0;  //need to reset stream
 130:              return cacheKey;
 131:          }
 132:   
 133:          private void SaveStreamToCache(string cacheKey, Stream stream)
 134:          {            
 135:              if (cacheDuration > 0) 
 136:              {
 137:                  Trace.WriteLine("Saving result in Cache");
 138:                  Trace.WriteLine("cacheKey = " + cacheKey);
 139:   
 140:                  MemoryStream ms = new MemoryStream();
 141:                  stream.Position = 0;
 142:                  CopyStream(stream, ms);
 143:                  ms.Position = 0;
 144:   
 145:                  HttpContext.Current.Cache.Insert(cacheKey, ms, null, 
 146:                      DateTime.Now.AddHours((double)cacheDuration), 
 147:                      TimeSpan.Zero);
 148:              }
 149:          }        
 150:   
 151:          private Stream GetStreamFromCache(string cacheKey)
 152:          {
 153:              Stream stream = HttpContext.Current.Cache[cacheKey] as Stream;
 154:              if (stream != null)
 155:              {
 156:                  stream.Position = 0;
 157:              }
 158:              return stream;
 159:          }
 160:   
 161:          private int GetCacheDuration(string cacheDurationConfigKey)
 162:          {
 163:              int cacheDurationValue = -1;
 164:              string cacheDurationStringValue = 
 165:                  ConfigurationSettings.AppSettings[cacheDurationConfigKey];
 166:              if (cacheDurationStringValue != null && 
 167:                  cacheDurationStringValue != string.Empty) 
 168:              {
 169:                  try
 170:                  {
 171:                      cacheDurationValue = int.Parse(cacheDurationStringValue);
 172:                      if (cacheDurationValue <=0)
 173:                      {
 174:                          Trace.WriteLine("Caching disabled. " +
 175:                              cacheDurationConfigKey + 
 176:                              " must be positive and greater than zero.");
 177:                      }
 178:                  }
 179:                  catch(Exception e)
 180:                  {
 181:                      Trace.WriteLine("Caching disabled. Unable to convert " +
 182:                          cacheDurationConfigKey + 
 183:                  " value from configuration file into int type. \nException: "
 184:                          + e.ToString());
 185:                  }
 186:              }
 187:              else
 188:              {
 189:                  Trace.WriteLine("Caching disabled. " + 
 190:                      cacheDurationConfigKey + 
 191:                      " value is null or empty string.");
 192:              }
 193:   
 194:              return cacheDurationValue;
 195:          }
 196:      }
 197:  }



You use the extension by adding the CacheExtension attribute to your
web service method:

   1:  [WebMethod]
   2:  [CacheExtension(CacheDurationConfigKey="GetDateTime.CacheDuration")]        
   3:  public string GetDateTime(string name)
   4:  {
   5:    return "Hello " + name + " the time is " + DateTime.Now.ToString();
   6:  }



Your web.config file should look something like this:
   1:    <appSettings>
   2:      <add key="DefaultCacheDuration" value="8"/>
   3:      <add key="GetDateTime.CacheDuration" value="2"/> 
   4:    </appSettings>



Some good links about Soap Extensions: