Azure Redis Cache with Storage Blobs Example

This example shows an implementation of Azure Redis Cache and Storage Blobs with the LEADTOOLS Document Library.

This is a fully-fledged example that can be used in a production environment. It uses Azure Storage Blobs to store binary data instead of storing directly to the cache. This approach is suitable if documents are to be accessible from multiple servers. For an example of saving large binary data directly to the cache, refer to Azure Redis Cache Example.

This example code requires the following NuGet packages:

In the Document Service source code, replace the code inside ServiceHelper.CreateCache with:

C#
// Get the Redis Cache database object to use 
var configuration = "your-cache-url.redis.cache.windows.net,password=your-password"; 
var configurationOptions = ConfigurationOptions.Parse(configuration); 
ConnectionMultiplexer connection = ConnectionMultiplexer.Connect(configurationOptions); 
IDatabase redisDatabase = connection.GetDatabase(); 
 
// Get the Blob Storage container to use for storing binary data 
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString")); 
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); 
// Container name can be anything, we chose document-service for this implementation 
string containerName = "document-service"; 
CloudBlobContainer container = blobClient.GetContainerReference(containerName); 
// Optional:  the container if it does not exist 
Task.Run(async () => await container.CreateIfNotExistsAsync()).Wait(); 
// Create our LEADTOOLS ObjectCache wrapper 
_cache = new RedisWithBlobsObjectCache(redisDatabase, blobClient, containerName); 

RedisWithBlobsObjectCache.cs

C#
using Leadtools; 
using Leadtools.Caching; 
using Leadtools.Codecs; 
using Leadtools.Svg; 
 
using Microsoft.WindowsAzure.Storage.Blob; 
using Newtonsoft.Json; 
using StackExchange.Redis; 
 
using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.IO; 
using System.Threading.Tasks; 
 
namespace MyNamespace 
{ 
   /// <summary> 
   /// Wraps a Redis Cache IDatabase object to be used with the LEADTOOLS Document Library. 
   /// </summary> 
   /// <remarks> 
   /// <para> 
   ///   This implementation adds support for storing the binary data into a Cloud Blob storage. 
   ///   The user specifies a container name and the binary data is stored in block blob with names = "regionName/key". 
   /// </para> 
   /// </remarks> 
   public class RedisWithBlobsObjectCache : ObjectCache 
   { 
      private RedisWithBlobsObjectCache() { } 
 
      /// <summary> 
      /// Initializes a LEADTOOLS Object Cache wrapper from a Redis cache database object. 
      /// </summary> 
      /// <param name="cache">Fully-initialized Redis database object ready to be used.</param> 
      /// <param name="blobClient">Azure blob client ready to be used.</param> 
      public RedisWithBlobsObjectCache(StackExchange.Redis.IDatabase cache, CloudBlobClient blobClient, string containerName = null) 
      { 
         this.Cache = cache; 
         this.BlobClient = blobClient; 
 
         // Blob storage container name to use 
         if (containerName == null) 
            this.ContainerName = Guid.NewGuid().ToString().Replace("-", ""); 
         else 
            this.ContainerName = containerName; 
      } 
 
      /// <summary> 
      /// The Redis cache database object being used. 
      /// </summary> 
      public StackExchange.Redis.IDatabase Cache { get; private set; } 
 
      /// <summary> 
      /// The Azure Blob client object being used. 
      /// </summary> 
      public CloudBlobClient BlobClient { get; private set; } 
 
      /// <summary> 
      /// The Azure Blob Container name being used. 
      /// </summary> 
      public string ContainerName { get; private set; } 
 
      // -------------------------------------------------------------------------------------- 
      // These members must be implemented by our class and are called by the Document toolkit 
      // -------------------------------------------------------------------------------------- 
 
      public override string Name 
      { 
         get 
         { 
            return "Redis with Blobs Object Cache"; 
         } 
      } 
 
      public override CacheSerializationMode PolicySerializationMode 
      { 
         get 
         { 
            // Redis do not use this so we will just assume it is binary 
            return CacheSerializationMode.Binary; 
         } 
 
         set { throw new NotSupportedException(); } 
      } 
 
      public override CacheSerializationMode DataSerializationMode 
      { 
         get 
         { 
            // Binary meaning we will do our own serialization 
            return CacheSerializationMode.Binary; 
         } 
         set { throw new NotSupportedException(); } 
      } 
 
      public override DefaultCacheCapabilities DefaultCacheCapabilities 
      { 
         get 
         { 
            // We support serialization: Meaning, the toolkit can send us "fat" .NET objects 
            // we will serialize them and not change the original reference 
            return DefaultCacheCapabilities.Serialization; 
         } 
      } 
 
      public override CacheItem<T> AddOrGetExisting<T>(CacheItem<T> item, CacheItemPolicy policy) 
      { 
         // Method called when a cache item is added. 
         // Must return the old value 
 
         // Resolve the key. Remember, we do not have regions 
         var resolvedKey = ResolveCacheKey(item.RegionName, item.Key); 
 
         CacheItem<T> oldItem = null; 
 
         // Get the old value (if any) 
         if (this.Cache.KeyExists(resolvedKey)) 
         { 
            RedisValue existingValue = this.Cache.StringGet(resolvedKey); 
            var oldValue = GetFromCacheAndBlobStorage<T>(existingValue, item.RegionName, item.Key); 
            oldItem = new CacheItem<T>(item.Key, (T)oldValue, item.RegionName); 
         } 
 
         // Add new value 
         AddToCacheAndBlobStorage(item, policy); 
 
         // Return old item 
         return oldItem; 
      } 
 
      public override CacheItem<T> GetCacheItem<T>(string key, string regionName) 
      { 
         // If we have an item with this key, return it. Otherwise, return null 
 
         var resolvedKey = ResolveCacheKey(regionName, key); 
 
         CacheItem<T> item = null; 
 
         if (this.Cache.KeyExists(resolvedKey)) 
         { 
            RedisValue value = this.Cache.StringGet(resolvedKey); 
            var itemValue = GetFromCacheAndBlobStorage<T>(value, regionName, key); 
            item = new CacheItem<T>(key, (T)itemValue, regionName); 
         } 
 
         return item; 
      } 
 
      public override bool Contains(string key, string regionName) 
      { 
         // Check if the key is in the dictionary 
 
         var resolvedKey = ResolveCacheKey(regionName, key); 
         var exists = this.Cache.KeyExists(resolvedKey); 
         return exists; 
      } 
 
      public override bool UpdateCacheItem<T>(CacheItem<T> item) 
      { 
         // Update the item 
 
         if (item == null) 
            throw new ArgumentNullException("item"); 
 
         var resolvedKey = ResolveCacheKey(item.RegionName, item.Key); 
         var exists = this.Cache.KeyExists(resolvedKey); 
         if (exists) 
         { 
            AddToCacheAndBlobStorage(item, null); 
         } 
 
         return exists; 
      } 
 
      public override T Remove<T>(string key, string regionName) 
      { 
         // Remove if found, return old value 
 
         T existingValue = default(T); 
         var resolvedKey = ResolveCacheKey(regionName, key); 
         if (this.Cache.KeyExists(resolvedKey)) 
         { 
            RedisValue value = this.Cache.StringGet(resolvedKey); 
            existingValue = (T)GetFromCacheAndBlobStorage<T>(value, regionName, key); 
         } 
 
         // Delete it 
         DeleteItem(key, regionName); 
 
         return existingValue; 
      } 
 
      public override void DeleteItem(string key, string regionName) 
      { 
         // Delete if found 
 
         DeleteFromCacheAndBlobStorage(regionName, key); 
      } 
 
      public override void UpdatePolicy(string key, CacheItemPolicy policy, string regionName) 
      { 
         // Redis Cache does not allow us to update the expiration policy of an item. 
      } 
 
      private static string ResolveCacheKey(string regionName, string key) 
      { 
         // Both must be non-empty strings 
         if (string.IsNullOrEmpty(regionName)) throw new InvalidOperationException("Region name must be a non-empty string"); 
         if (string.IsNullOrEmpty(key)) throw new InvalidOperationException("Region key name must be a non-empty string"); 
 
         // regionName might not be unique, key might not be unique, but combine them and we are guaranteed a unique key 
         return regionName + "-" + key; 
      } 
 
      public static string ResolveBlobReference(string regionName, string key) 
      { 
         // Get the blob reference 
         return regionName + "/" + key; 
      } 
 
      private void AddToCacheAndBlobStorage<T>(CacheItem<T> item, CacheItemPolicy policy) 
      { 
         // Get a Redis value from our item data 
         byte[] blob = null; 
         bool hasBlob = false; 
         string json = null; 
 
         var typeOfT = typeof(T); 
 
         if (typeOfT == typeof(RasterImage)) 
         { 
            blob = ImageToBlob(item.Value as RasterImage); 
            hasBlob = true; 
         } 
         else if (typeOfT == typeof(SvgDocument)) 
         { 
            blob = SvgToBlob(item.Value as SvgDocument); 
            hasBlob = true; 
         } 
         else if (typeOfT == typeof(byte[])) 
         { 
            blob = item.Value as byte[]; 
            hasBlob = true; 
         } 
         else 
         { 
            // JSON serialize it 
            json = JsonConvert.SerializeObject(item.Value); 
            hasBlob = false; 
         } 
 
         // If the sliding expiration is used, make it the absolute value 
         TimeSpan? expiry = null; 
 
         if (policy != null) 
         { 
            var expiryDate = policy.AbsoluteExpiration; 
            if (policy.SlidingExpiration > TimeSpan.Zero) 
            { 
               expiryDate = DateTime.UtcNow.Add(policy.SlidingExpiration); 
            } 
 
            // Now, we have a date, convert it to time span from now (all UTC) 
            expiry = expiryDate.Subtract(DateTime.UtcNow); 
         } 
 
         var resolvedKey = ResolveCacheKey(item.RegionName, item.Key); 
 
         // Set the cache item value 
         if (hasBlob) 
         { 
            this.Cache.StringSet(resolvedKey, 1, expiry); 
            if (blob != null) 
            { 
               var blobReference = ResolveBlobReference(item.RegionName, item.Key); 
               CloudBlobContainer container = this.BlobClient.GetContainerReference(this.ContainerName); 
               CloudBlockBlob blockBlob = container.GetBlockBlobReference(blobReference); 
               Task.Run(async () => await blockBlob.UploadFromByteArrayAsync(blob, 0, blob.Length)).Wait(); 
            } 
         } 
         else 
         { 
            this.Cache.StringSet(resolvedKey, json, expiry); 
         } 
      } 
 
      private object GetFromCacheAndBlobStorage<T>(RedisValue value, string regionName, string key) 
      { 
         var typeOfT = typeof(T); 
         object result = null; 
 
         if (typeOfT == typeof(RasterImage) || typeOfT == typeof(SvgDocument) || typeOfT == typeof(byte[])) 
         { 
            // Read the blob  
            CloudBlobContainer container = this.BlobClient.GetContainerReference(this.ContainerName); 
            CloudBlockBlob blockBlob = container.GetBlockBlobReference(ResolveBlobReference(regionName, key)); 
            bool exists = Task.Run(async () => await blockBlob.ExistsAsync()).Result; 
            if(exists) 
               Task.Run(async () => await blockBlob.FetchAttributesAsync()).Wait(); 
 
            byte[] blob = null; 
            var length = blockBlob.Properties.Length; 
            if (length != -1) 
            { 
               blob = new byte[length]; 
               if (length > 0) 
                  Task.Run(async () => await blockBlob.DownloadToByteArrayAsync(blob, 0)).Wait(); 
            } 
 
            if (typeOfT == typeof(RasterImage)) 
            { 
               result = ImageFromBlob(blob); 
            } 
            else if (typeOfT == typeof(SvgDocument)) 
            { 
               result = SvgFromBlob(blob); 
            } 
            else 
            { 
               result = (byte[])blob; 
            } 
         } 
         else 
         { 
            // JSON deserialize it  
            result = JsonConvert.DeserializeObject<T>(value); 
         } 
 
         return result; 
      } 
 
      private void DeleteFromCacheAndBlobStorage(string regionName, string key) 
      { 
         var resolvedKey = ResolveCacheKey(regionName, key); 
         if (this.Cache.KeyExists(resolvedKey)) 
            this.Cache.KeyDelete(resolvedKey); 
 
         CloudBlobContainer container = this.BlobClient.GetContainerReference(this.ContainerName); 
         CloudBlockBlob blockBlob = container.GetBlockBlobReference(ResolveBlobReference(regionName, key)); 
         Task.Run(async () => await blockBlob.DeleteIfExistsAsync()).Wait(); 
      } 
 
      // Helper methods to convert RasterImage or SvgDocument objects from/to byte[] 
      private static byte[] ImageToBlob(RasterImage image) 
      { 
         if (image == null) 
            return null; 
 
         // Save as PNG into a memory stream, use the byte[] data 
         using (var rasterCodecs = new RasterCodecs()) 
         { 
            using (var ms = new MemoryStream()) 
            { 
               rasterCodecs.Save(image, ms, RasterImageFormat.Png, 0); 
               return ms.GetBuffer(); 
            } 
         } 
      } 
 
      private static RasterImage ImageFromBlob(byte[] blob) 
      { 
         if (blob == null || blob.Length == 0) 
            return null; 
 
         // Load to a raster image, using the byte[] data 
         using (var rasterCodecs = new RasterCodecs()) 
         { 
            using (var ms = new MemoryStream(blob)) 
            { 
               return rasterCodecs.Load(ms, 1); 
            } 
         } 
      } 
 
      private static byte[] SvgToBlob(SvgDocument svg) 
      { 
         if (svg == null) 
            return null; 
 
 
         using (var ms = new MemoryStream()) 
         { 
            svg.SaveToStream(ms, null); 
            return ms.GetBuffer(); 
         } 
      } 
 
      private static SvgDocument SvgFromBlob(byte[] blob) 
      { 
         if (blob == null || blob.Length == 0) 
            return null; 
 
         return SvgDocument.LoadFromMemory(blob, 0, blob.Length, null); 
      } 
 
      // 
      // These members must be over-ridden by our class but are never called by the Document toolkit 
      // So just throw a not supported exception 
      // 
 
      // This is for default region support. We do not have that 
      public override object this[string key] 
      { 
         get 
         { 
            throw new NotSupportedException(); 
         } 
         set 
         { 
            throw new NotSupportedException(); 
         } 
      } 
 
      // Delete a region in one shot. We do not support that 
      // Note: This is only called if we have DefaultCacheCapabilities.CacheRegions. Since we do not, the caller is responsible for 
      // calling DeleteAll passing all the items of the region (which in turn will call DeleteItem for each) 
      public override void DeleteRegion(string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Begin adding an external resource. We do not support that 
      // Note: This is only called if we have DefaultCacheCapabilities.ExternalResources 
      public override Uri BeginAddExternalResource(string key, string regionName, bool readWrite) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // End adding an external resource. We do not support that 
      // Note: This is only called if we have DefaultCacheCapabilities.ExternalResources 
      public override void EndAddExternalResource<T>(bool commit, string key, T value, CacheItemPolicy policy, string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Get the item external resource. We do not support that 
      // Note: This is only called if we have DefaultCacheCapabilities.ExternalResources 
      public override Uri GetItemExternalResource(string key, string regionName, bool readWrite) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Remove the item external resource. We do not support that 
      // Note: This is only called if we have DefaultCacheCapabilities.ExternalResources 
      public override void RemoveItemExternalResource(string key, string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Get the item virtual directory path. We do not support that 
      // Note: This is only called if we have DefaultCacheCapabilities.VirtualDirectory 
      public override Uri GetItemVirtualDirectoryUrl(string key, string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Getting number of items in the cache. We do not support that 
      public override long GetCount(string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Statistics. We do not support that 
      public override CacheStatistics GetStatistics() 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Statistics. We do not support that 
      public override CacheStatistics GetStatistics(string key, string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Getting all the values. We do not support that 
      public override IDictionary<string, object> GetValues(IEnumerable<string> keys, string regionName) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Enumeration of the items. We do not support that 
      protected override IEnumerator<KeyValuePair<string, object>> GetEnumerator() 
      { 
         throw new NotSupportedException(); 
      } 
 
 
      // Enumeration of the keys. We do not support that 
      public override void EnumerateKeys(string region, EnumerateCacheEntriesCallback callback) 
      { 
         throw new NotSupportedException(); 
      } 
 
      // Enumeration of regions. We do not support that 
      public override void EnumerateRegions(EnumerateCacheEntriesCallback callback) 
      { 
         throw new NotSupportedException(); 
      } 
   } 
} 

Help Version 22.0.2023.7.17
Products | Support | Contact Us | Intellectual Property Notices
© 1991-2023 LEAD Technologies, Inc. All Rights Reserved.

LEADTOOLS Imaging, Medical, and Document
Products | Support | Contact Us | Intellectual Property Notices
© 1991-2023 LEAD Technologies, Inc. All Rights Reserved.