A common requirement in many development scenarios is caching. In BizTalk implementations, this can be the case, mainly for performance reasons.
As a test I wrote 2 pipelinecomponents that handle 2 common issues with caching inside BizTalk. These sample components are performing similar tasks to two components of the Codit implementation framework. This is a framework we use at a lot of our customers.
- Code table mappings : limit access to SQL database to perform Code table mappings. In our framework, this is similar to the Transco component.
- Duplicate message : stop messages that come into BizTalk multiple times within a specific timeframe. In our framework, this is similar to the Double checker component.
CacheHelper
Because both pipelinecomponents use the AppFabric cache, I wrote a small class that takes care of this.
public class CacheHelper : IDisposable { private string _cacheName = "default"; private string _region; public CacheHelper(string region) { _region = region; CreateRegion(_cacheName,region); } /// /// Creates a Region in a specified cache. /// /// Cache name /// Region name private void CreateRegion(string cacheName,string region) { DataCacheFactory dcf=ConnectToCache(); if (dcf != null) { DataCache dc=dcf.GetCache(cacheName); dc.CreateRegion(region); } } /// /// Connect to a Cache server /// /// The Datacache private DataCacheFactory ConnectToCache() { //This can also be kept in a config file var config = new DataCacheFactoryConfiguration(); config.SecurityProperties = new DataCacheSecurity(DataCacheSecurityMode.None, DataCacheProtectionLevel.None); config.Servers = new List { new DataCacheServerEndpoint(Environment.MachineName, 22233) }; return new DataCacheFactory(config); } ~CacheHelper() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } /// /// Gets a value from the cache in the specified region (class level) /// /// Key linked to the data /// The found data. If null --> not found in the cache public string GetLookUpCacheData(string keyValue) { DataCacheFactory dcf = ConnectToCache(); var cache = dcf.GetCache(_cacheName); string data = cache.Get(keyValue,_region) as string; return data; } /// /// Store a value in the cache /// /// Key /// Data public void StoreLookUpCacheData(string keyValue, object value) { DataCacheFactory dcf = ConnectToCache(); var cache = dcf.GetCache(_cacheName); cache.Add(keyValue, value, _region); } /// /// Stores a value in the cache for a specified amount of time /// /// Key /// Data /// Time to keep the data in the cache public void StoreLookUpCacheData(string keyValue, object value,TimeSpan expires) { DataCacheFactory dcf = ConnectToCache(); var cache = dcf.GetCache(_cacheName); cache.Add(keyValue, value, expires, _region); } }
This is a very simple implementation that will store values in the default cache and in a specified region.
CodeTable Mapper
Codetable mapping is a very common scenario in BizTalk implementations. In my example we will be translating countrycodes to the country name.
The values are stored in a SQL table. But every time we get a value, we are going to save it to the AppFabric cache.
When we want to get the same value again, we are not going to the database but we will get the stored value from the AppFabric Cache.
public IBaseMessage Execute(IPipelineContext pContext, IBaseMessage pInMsg) { // Set variables biztalkMessage = pInMsg; XmlReader reader = XmlReader.Create(pInMsg.BodyPart.Data); XPathCollection xpaths = new XPathCollection(); //For this example we are going to use 1 xpath expression xpaths.Add(this.XPath); ValueMutator vm = new ValueMutator(handleXpathFound); pInMsg.BodyPart.Data = new XPathMutatorStream(reader, xpaths, vm); return pInMsg; } private void handleXpathFound(int matchIdx, XPathExpression matchExpr, string origVal, ref string finalVal) { CacheHelper ch = new CacheHelper("Countries"); string data = ch.GetLookUpCacheData(origVal); if (data == null) { finalVal = GetCountryFromDB(origVal); ch.StoreLookUpCacheData(origVal, finalVal); } else finalVal = ch.GetLookUpCacheData(origVal); } private string GetCountryFromDB(string countryCode) { string country = string.Empty; SqlConnection conn = null; SqlCommand comm = null; try { //Connect to look up database and retrieve the names of the products. conn = new SqlConnection("Data Source=(local);Initial Catalog=CacheTest;Integrated Security=SSPI;"); conn.Open(); comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = string.Format("SELECT Country FROM Countries WHERE CountryCode='{0}'", countryCode); comm.CommandType = CommandType.Text; country = (string)comm.ExecuteScalar(); if(string.IsNullOrEmpty(country)) throw new Exception(string.Format("No country found for code {0}",countryCode)); } catch (Exception e) { throw new Exception(e.Message + e.StackTrace); } finally { comm.Dispose(); conn.Close(); conn.Dispose(); } return country; }
Duplicate Message Checker
As a sample scenario I took one I read about a few months ago. BizTalk had to stop messages that come in multiple times within 2 minutes.
So if there are more then 2 minutes between the messages, they should continue.
Normally this would involve a SQL table to store some information and some job to do the cleanup of this table.
But for my example I use AppFabric cache. There you have the option to store something in the cache for a certain timespan.
It is automatically deleted after this period.
public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(IPipelineContext pContext, Microsoft.BizTalk.Message.Interop.IBaseMessage pInMsg) { //Create hash VirtualStream input = new VirtualStream(pInMsg.BodyPart.GetOriginalDataStream()); MD5 md5 = MD5.Create(); byte[] hash = md5.ComputeHash(input); string hashString = Convert.ToBase64String(hash); //check Cache CacheHelper ch = new CacheHelper("DuplicateMessages"); string date=ch.GetLookUpCacheData(hashString); if (string.IsNullOrEmpty(date)) { //If not in cache yet, store it --> lifetime is 2 minutes ch.StoreLookUpCacheData(hashString, DateTime.Now.ToString(), new TimeSpan(0, 2, 0)); } else { //Throw error throw new ApplicationException(string.Format("Duplicate Message. Already received at {0}",date)); } //Put stream back to beginning input.Seek(0, SeekOrigin.Begin); return pInMsg; }
This makes the implementation very easy and you will not need a SQL table or anything to store the information.
You could say that you can do this with a custom caching solution as well. But what about HA environments with multiple BizTalk servers?
AppFabric is a distributed cache. So it doesn’t mather on which server the message is processed. It will end up in the same cache and will be accessible on all the servers.
Conclusion
As you see, AppFabric caching has some advantages in BizTalk as well. The API is very easy to use and I got this to work quite quickly.
Tim D’haeyer, CODit