Devesprit.ImageProcessor & Devesprit.ImageServer
Cache

IImageCache

The IImageCache defines methods and properties which allow developers to extend Devesprit.ImageServer to persist cached images in alternate locations. For example: Azure Blob Containers.

IImageCache.cs
Copy Code
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Web;
namespace Devesprit.ImageServer.Caching
{
    /// <summary>
    ///  Defines properties and methods for allowing caching of images to different sources.
    /// </summary>
    public interface IImageCache
    {
        /// <summary>
        /// Gets or sets any additional settings required by the cache.
        /// </summary>
        Dictionary<string, string> Settings { get; set; }
        /// <summary>
        /// Gets the path to the cached image.
        /// </summary>
        string CachedPath { get; }
        /// <summary>
        /// Gets or sets the maximum number of days to store the image.
        /// </summary>
        int MaxDays { get; set; }
        /// <summary>
        /// Gets or sets the maximum number of days to cache the image in the browser.
        /// </summary>
        int BrowserMaxDays { get; set; }
        /// <summary>
        /// Gets a value indicating whether the image is new or updated in an asynchronous manner.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> returning the value.
        /// </returns>
        Task<bool> IsNewOrUpdatedAsync();
        /// <summary>
        /// Adds the image to the cache in an asynchronous manner.
        /// </summary>
        /// <param name="stream">
        /// The stream containing the image data.
        /// </param>
        /// <param name="contentType">
        /// The content type of the image.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/> representing an asynchronous operation.
        /// </returns>
        Task AddImageToCacheAsync(Stream stream, string contentType);
        /// <summary>
        /// Trims the cache of any expired items in an asynchronous manner.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> representing an asynchronous operation.
        /// </returns>
        Task TrimCacheAsync();
        /// <summary>
        /// Gets a string identifying the cached file name in an asynchronous manner.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> returning the value.
        /// </returns>
        Task<string> CreateCachedFileNameAsync();
        /// <summary>
        /// Rewrites the path to point to the cached image.
        /// </summary>
        /// <param name="context">
        /// The <see cref="HttpContext"/> encapsulating all information about the request.
        /// </param>
        void RewritePath(HttpContext context);
    }
}

Example

AzureBlobCache.cs

AzureBlobCache.cs
Copy Code
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Devesprit.ImageProcessor.Configuration;
using Devesprit.ImageServer.Caching;
using Devesprit.ImageServer.HttpModules;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
namespace Devesprit.ImageServer.Plugins.AzureBlobCache
{
    /// <summary>
    /// Provides an <see cref="IImageCache"/> implementation that uses Azure blob storage.
    /// The cache is self healing and cleaning.
    /// </summary>
    public class AzureBlobCache : ImageCacheBase
    {
        /// <summary>
        /// The regular expression for parsing a remote uri.
        /// </summary>
        private static readonly Regex RemoteRegex = new Regex("^http(s)?://", RegexOptions.Compiled);
        /// <summary>
        /// The assembly version.
        /// </summary>
        private static readonly string AssemblyVersion =
            typeof(ImageProcessingModule).Assembly.GetName().Version.ToString();
        /// <summary>
        /// The cloud blob client, thread-safe so can be re-used
        /// </summary>
        private static CloudBlobClient cloudCachedBlobClient;
        /// <summary>
        /// The cloud cached blob container.
        /// </summary>
        private static CloudBlobContainer cloudCachedBlobContainer;
        /// <summary>
        /// The cloud source blob container.
        /// </summary>
        private static CloudBlobContainer cloudSourceBlobContainer;
        /// <summary>
        /// The cached root url for a content delivery network.
        /// </summary>
        private readonly string cachedCdnRoot;
        /// <summary>
        /// Determines if the CDN request is redirected or rewritten
        /// </summary>
        private readonly bool streamCachedImage;
        /// <summary>
        /// The timeout length for requesting the CDN url.
        /// </summary>
        private readonly int timeout = 1000;
        /// <summary>
        /// The cached rewrite path.
        /// </summary>
        private string cachedRewritePath;
        /// <summary>
        /// Initializes a new instance of the <see cref="AzureBlobCache"/> class.
        /// </summary>
        /// <param name="requestPath">
        /// The request path for the image.
        /// </param>
        /// <param name="fullPath">
        /// The full path for the image.
        /// </param>
        /// <param name="querystring">
        /// The query string containing instructions.
        /// </param>
        public AzureBlobCache(string requestPath, string fullPath, string querystring)
            : base(requestPath, fullPath, querystring)
        {
            if (cloudCachedBlobClient == null)
            {
                // Retrieve storage accounts from connection string.
                CloudStorageAccount cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]);
                // Create the blob clients.
                cloudCachedBlobClient = cloudCachedStorageAccount.CreateCloudBlobClient();
            }
            if (cloudCachedBlobContainer == null)
            {
                // Retrieve references to a container.
                cloudCachedBlobContainer = CreateContainer(cloudCachedBlobClient, this.Settings["CachedBlobContainer"], BlobContainerPublicAccessType.Blob);
            }
            if (cloudSourceBlobContainer == null)
            {
                string sourceAccount = this.Settings.ContainsKey("SourceStorageAccount")
                                           ? this.Settings["SourceStorageAccount"]
                                           : string.Empty;
                // Repeat for source if it exists
                if (!string.IsNullOrWhiteSpace(sourceAccount))
                {
                    CloudStorageAccount cloudSourceStorageAccount = CloudStorageAccount.Parse(this.Settings["SourceStorageAccount"]);
                    CloudBlobClient cloudSourceBlobClient = cloudSourceStorageAccount.CreateCloudBlobClient();
                    cloudSourceBlobContainer = cloudSourceBlobClient.GetContainerReference(this.Settings["SourceBlobContainer"]);
                }
            }
            this.cachedCdnRoot = this.Settings.ContainsKey("CachedCDNRoot")
                                     ? this.Settings["CachedCDNRoot"]
                                     : cloudCachedBlobContainer.Uri.ToString().TrimEnd(cloudCachedBlobContainer.Name.ToCharArray());
            if (this.Settings.ContainsKey("CachedCDNTimeout"))
            {
                int t;
                int.TryParse(this.Settings["CachedCDNTimeout"], out t);
                this.timeout = t;
            }
            // This setting was added to facilitate streaming of the blob resource directly instead of a redirect. This is beneficial for CDN purposes
            // but caution should be taken if not used with a CDN as it will add quite a bit of overhead to the site.
            this.streamCachedImage = this.Settings.ContainsKey("StreamCachedImage") && this.Settings["StreamCachedImage"].ToLower() == "true";
        }
        /// <summary>
        /// Gets a value indicating whether the image is new or updated in an asynchronous manner.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> returning the value.
        /// </returns>
        public override async Task<bool> IsNewOrUpdatedAsync()
        {
            // if the last time it was checked is greater than 5 seconds. This would be much better for perf
            // if there is a high throughput of image requests.
            string cachedFileName = await this.CreateCachedFileNameAsync();
            this.CachedPath = CachedImageHelper.GetCachedPath(cloudCachedBlobContainer.Uri.ToString(), cachedFileName, true, this.FolderDepth);
            // Do we insert the cache container? This seems to break some setups.
            bool useCachedContainerInUrl = this.Settings.ContainsKey("UseCachedContainerInUrl") && this.Settings["UseCachedContainerInUrl"].ToLower() != "false";
            this.cachedRewritePath = CachedImageHelper.GetCachedPath(useCachedContainerInUrl ? Path.Combine(this.cachedCdnRoot, cloudCachedBlobContainer.Name) : this.cachedCdnRoot, cachedFileName, true, this.FolderDepth);
            bool isUpdated = false;
            CachedImage cachedImage = CacheIndexer.Get(this.CachedPath);
            if (new Uri(this.CachedPath).IsFile)
            {
                if (File.Exists(this.CachedPath))
                {
                    cachedImage = new CachedImage
                    {
                        Key = Path.GetFileNameWithoutExtension(this.CachedPath),
                        Path = this.CachedPath,
                        CreationTimeUtc = File.GetCreationTimeUtc(this.CachedPath)
                    };
                    CacheIndexer.Add(cachedImage);
                }
            }
            if (cachedImage == null)
            {
                string blobPath = this.CachedPath.Substring(cloudCachedBlobContainer.Uri.ToString().Length + 1);
                CloudBlockBlob blockBlob = cloudCachedBlobContainer.GetBlockBlobReference(blobPath);
                if (await blockBlob.ExistsAsync())
                {
                    // Pull the latest info.
                    await blockBlob.FetchAttributesAsync();
                    if (blockBlob.Properties.LastModified.HasValue)
                    {
                        cachedImage = new CachedImage
                        {
                            Key = Path.GetFileNameWithoutExtension(this.CachedPath),
                            Path = this.CachedPath,
                            CreationTimeUtc = blockBlob.Properties.LastModified.Value.UtcDateTime
                        };
                        CacheIndexer.Add(cachedImage);
                    }
                }
            }
            if (cachedImage == null)
            {
                // Nothing in the cache so we should return true.
                isUpdated = true;
            }
            else
            {
                // Check to see if the cached image is set to expire
                // or a new file with the same name has replaced our current image
                if (this.IsExpired(cachedImage.CreationTimeUtc) || await this.IsUpdatedAsync(cachedImage.CreationTimeUtc))
                {
                    CacheIndexer.Remove(this.CachedPath);
                    isUpdated = true;
                }
            }
            return isUpdated;
        }
        /// <summary>
        /// Adds the image to the cache in an asynchronous manner.
        /// </summary>
        /// <param name="stream">
        /// The stream containing the image data.
        /// </param>
        /// <param name="contentType">
        /// The content type of the image.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/> representing an asynchronous operation.
        /// </returns>
        public override async Task AddImageToCacheAsync(Stream stream, string contentType)
        {
            string blobPath = this.CachedPath.Substring(cloudCachedBlobContainer.Uri.ToString().Length + 1);
            CloudBlockBlob blockBlob = cloudCachedBlobContainer.GetBlockBlobReference(blobPath);
            await blockBlob.UploadFromStreamAsync(stream);
            blockBlob.Properties.ContentType = contentType;
            blockBlob.Properties.CacheControl = $"public, max-age={this.BrowserMaxDays * 86400}";
            await blockBlob.SetPropertiesAsync();
            blockBlob.Metadata.Add("ImageProcessedBy", "Devesprit.ImageServer/" + AssemblyVersion);
            await blockBlob.SetMetadataAsync();
        }
        /// <summary>
        /// Trims the cache of any expired items in an asynchronous manner.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> representing an asynchronous operation.
        /// </returns>
        public override Task TrimCacheAsync()
        {
            if (!this.TrimCache)
            {
                return Task.FromResult(0);
            }
            this.ScheduleCacheTrimmer(async token =>
            {
                // Jump up to the parent branch to clean through the cache.
                string parent = string.Empty;
                if (this.FolderDepth > 0)
                {
                    Uri uri = new Uri(this.CachedPath);
                    string path = uri.GetLeftPart(UriPartial.Path).Substring(cloudCachedBlobContainer.Uri.ToString().Length + 1);
                    parent = path.Substring(0, 2);
                }
                BlobContinuationToken continuationToken = null;
                List<IListBlobItem> results = new List<IListBlobItem>();
                // Loop through the all the files in a non blocking fashion.
                do
                {
                    BlobResultSegment response = await cloudCachedBlobContainer.ListBlobsSegmentedAsync(parent, true, BlobListingDetails.Metadata, 5000, continuationToken, null, null, token);
                    continuationToken = response.ContinuationToken;
                    results.AddRange(response.Results);
                }
                while (token.IsCancellationRequested == false && continuationToken != null);
                // Now leap through and delete.
                foreach (
                    CloudBlockBlob blob in
                    results.Where((blobItem, type) => blobItem is CloudBlockBlob)
                           .Cast<CloudBlockBlob>()
                           .OrderBy(b => b.Properties.LastModified?.UtcDateTime ?? new DateTime()))
                {
                    if (token.IsCancellationRequested || (blob.Properties.LastModified.HasValue && !this.IsExpired(blob.Properties.LastModified.Value.UtcDateTime)))
                    {
                        break;
                    }
                    // Remove from the cache and delete each CachedImage.
                    CacheIndexer.Remove(blob.Name);
                    await blob.DeleteAsync(token);
                }
            });
            return Task.FromResult(0);
        }
        /// <summary>
        /// Returns a value indicating whether the requested image has been updated.
        /// </summary>
        /// <param name="creationDate">The creation date.</param>
        /// <returns>The <see cref="bool"/></returns>
        private async Task<bool> IsUpdatedAsync(DateTime creationDate)
        {
            bool isUpdated = false;
            try
            {
                if (new Uri(this.RequestPath).IsFile)
                {
                    if (File.Exists(this.RequestPath))
                    {
                        // If it's newer than the cached file then it must be an update.
                        isUpdated = File.GetLastWriteTimeUtc(this.RequestPath) > creationDate;
                    }
                }
                else if (cloudSourceBlobContainer != null)
                {
                    string container = RemoteRegex.Replace(cloudSourceBlobContainer.Uri.ToString(), string.Empty);
                    string blobPath = RemoteRegex.Replace(this.RequestPath, string.Empty);
                    blobPath = blobPath.Replace(container, string.Empty).TrimStart('/');
                    CloudBlockBlob blockBlob = cloudSourceBlobContainer.GetBlockBlobReference(blobPath);
                    if (await blockBlob.ExistsAsync())
                    {
                        // Pull the latest info.
                        await blockBlob.FetchAttributesAsync();
                        if (blockBlob.Properties.LastModified.HasValue)
                        {
                            isUpdated = blockBlob.Properties.LastModified.Value.UtcDateTime > creationDate;
                        }
                    }
                }
                else
                {
                    // Try and get the headers for the file, this should allow cache busting for remote files.
                    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.RequestPath);
                    request.Method = "HEAD";
                    using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync())
                    {
                        isUpdated = response.LastModified.ToUniversalTime() > creationDate;
                    }
                }
            }
            catch
            {
                isUpdated = false;
            }
            return isUpdated;
        }
        /// <summary>
        /// Rewrites the path to point to the cached image.
        /// </summary>
        /// <param name="context">
        /// The <see cref="HttpContext"/> encapsulating all information about the request.
        /// </param>
        public override void RewritePath(HttpContext context)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.cachedRewritePath);
            if (this.streamCachedImage)
            {
                // Map headers to enable 304s to pass through
                if (context.Request.Headers["If-Modified-Since"] != null)
                {
                    TrySetIfModifiedSinceDate(context, request);
                }
                string[] mapRequestHeaders = { "Cache-Control", "If-None-Match" };
                foreach (string h in mapRequestHeaders)
                {
                    if (context.Request.Headers[h] != null)
                    {
                        request.Headers.Add(h, context.Request.Headers[h]);
                    }
                }
                // Write the blob storage directly to the stream
                request.Method = "GET";
                request.Timeout = this.timeout;
                HttpWebResponse response = null;
                try
                {
                    response = (HttpWebResponse)request.GetResponse();
                }
                catch (WebException ex)
                {
                    // A 304 is not an error
                    if (ex.Response != null && ((HttpWebResponse)ex.Response).StatusCode == HttpStatusCode.NotModified)
                    {
                        response = (HttpWebResponse)ex.Response;
                    }
                    else
                    {
                        response?.Dispose();
                        ImageProcessorBootstrapper.Instance.Logger.Log<AzureBlobCache>("Unable to stream cached path: " + this.cachedRewritePath);
                        return;
                    }
                }
                Stream cachedStream = response.GetResponseStream();
                if (cachedStream != null)
                {
                    HttpResponse contextResponse = context.Response;
                    // If streaming but not using a CDN the headers will be null.
                    string etagHeader = response.Headers["ETag"];
                    if (!string.IsNullOrWhiteSpace(etagHeader))
                    {
                        contextResponse.Headers.Add("ETag", etagHeader);
                    }
                    string lastModifiedHeader = response.Headers["Last-Modified"];
                    if (!string.IsNullOrWhiteSpace(lastModifiedHeader))
                    {
                        contextResponse.Headers.Add("Last-Modified", lastModifiedHeader);
                    }
                    cachedStream.CopyTo(contextResponse.OutputStream); // Will be empty on 304s
                    ImageProcessingModule.SetHeaders(
                        context,
                        response.StatusCode == HttpStatusCode.NotModified ? null : response.ContentType,
                        null,
                        this.BrowserMaxDays,
                        response.StatusCode);
                }
                cachedStream?.Dispose();
                response.Dispose();
            }
            else
            {
                // Redirect the request to the blob URL
                request.Method = "HEAD";
                request.Timeout = this.timeout;
                HttpWebResponse response;
                try
                {
                    response = (HttpWebResponse)request.GetResponse();
                    response.Dispose();
                    ImageProcessingModule.AddCorsRequestHeaders(context);
                    context.Response.Redirect(this.cachedRewritePath, false);
                }
                catch (WebException ex)
                {
                    response = (HttpWebResponse)ex.Response;
                    if (response != null)
                    {
                        HttpStatusCode responseCode = response.StatusCode;
                        // A 304 is not an error
                        if (responseCode == HttpStatusCode.NotModified)
                        {
                            response.Dispose();
                            ImageProcessingModule.AddCorsRequestHeaders(context);
                            context.Response.Redirect(this.cachedRewritePath, false);
                        }
                        else
                        {
                            response.Dispose();
                            ImageProcessorBootstrapper.Instance.Logger.Log<AzureBlobCache>("Unable to rewrite cached path to: " + this.cachedRewritePath);
                        }
                    }
                    else
                    {
                        // It's a 404, we should redirect to the cached path we have just saved to.
                        ImageProcessingModule.AddCorsRequestHeaders(context);
                        context.Response.Redirect(this.CachedPath, false);
                    }
                }
            }
        }
        /// <summary>
        /// Tries to set IfModifiedSince header however this crashes when context.Request.Headers["If-Modified-Since"] exists,
        /// but cannot be parsed. It cannot be parsed when it comes from Google Bot as UTC <example>Sun, 27 Nov 2016 20:01:45 UTC</example>
        /// so DateTime.TryParse. If it returns false, then log the error.
        /// </summary>
        /// <param name="context">The current context</param>
        /// <param name="request">The current request</param>
        private static void TrySetIfModifiedSinceDate(HttpContext context, HttpWebRequest request)
        {
            DateTime ifModifiedDate;
            string ifModifiedFromRequest = context.Request.Headers["If-Modified-Since"];
            if (DateTime.TryParse(ifModifiedFromRequest, out ifModifiedDate))
            {
                request.IfModifiedSince = ifModifiedDate;
            }
            else
            {
                if (ifModifiedFromRequest.ToLower().Contains("utc"))
                {
                    ifModifiedFromRequest = ifModifiedFromRequest.ToLower().Replace("utc", string.Empty);
                    if (DateTime.TryParse(ifModifiedFromRequest, out ifModifiedDate))
                    {
                        request.IfModifiedSince = ifModifiedDate;
                    }
                }
                else
                {
                    ImageProcessorBootstrapper.Instance.Logger.Log<AzureBlobCache>($"Unable to parse date {context.Request.Headers["If-Modified-Since"]} for {context.Request.Url}");
                }
            }
        }
        /// <summary>
        /// Returns the cache container, creating a new one if none exists.
        /// </summary>
        /// <param name="cloudBlobClient"><see cref="CloudBlobClient"/> where the container is stored.</param>
        /// <param name="containerName">The name of the container.</param>
        /// <param name="accessType"><see cref="BlobContainerPublicAccessType"/> indicating the access permissions.</param>
        /// <returns>The <see cref="CloudBlobContainer"/></returns>
        private static CloudBlobContainer CreateContainer(CloudBlobClient cloudBlobClient, string containerName, BlobContainerPublicAccessType accessType)
        {
            CloudBlobContainer container = cloudBlobClient.GetContainerReference(containerName);
            if (!container.Exists())
            {
                container.Create();
                container.SetPermissions(new BlobContainerPermissions { PublicAccess = accessType });
            }
            return container;
        }
    }
}

ImageCacheBase.cs

ImageCacheBase.cs
Copy Code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using Devesprit.ImageServer.Configuration;
namespace Devesprit.ImageServer.Caching
{
    /// <summary>
    /// The image cache base provides methods for implementing the <see cref="IImageCacheExtended"/> interface.
    /// It is recommended that any implementations inherit from this class.
    /// </summary>
    public abstract class ImageCacheBase : IImageCacheExtended
    {
        private static readonly CacheTrimmer Trimmer = new CacheTrimmer();
        /// <summary>
        /// The request path for the image.
        /// </summary>
        protected readonly string RequestPath;
        /// <summary>
        /// The full path for the image.
        /// </summary>
        protected readonly string FullPath;
        /// <summary>
        /// The querystring containing processing instructions.
        /// </summary>
        protected readonly string Querystring;
        /// <summary>
        /// Initializes a new instance of the <see cref="ImageCacheBase"/> class.
        /// </summary>
        /// <param name="requestPath">
        /// The request path for the image.
        /// </param>
        /// <param name="fullPath">
        /// The full path for the image.
        /// </param>
        /// <param name="querystring">
        /// The querystring containing instructions.
        /// </param>
        protected ImageCacheBase(string requestPath, string fullPath, string querystring)
        {
            this.RequestPath = requestPath;
            this.FullPath = fullPath;
            this.Querystring = querystring;
            ImageProcessorConfiguration config = ImageProcessorConfiguration.Instance;
            this.Settings = this.AugmentSettingsCore(config.ImageCacheSettings);
            this.MaxDays = config.ImageCacheMaxDays;
            this.BrowserMaxDays = config.BrowserCacheMaxDays;
            this.TrimCache = config.TrimCache;
            this.FolderDepth = config.FolderDepth;
        }
        /// <summary>
        /// Gets or sets any additional settings required by the cache.
        /// </summary>
        public Dictionary<string, string> Settings { get; set; }
        /// <summary>
        /// Gets or sets the path to the cached image.
        /// </summary>
        public string CachedPath { get; set; }
        /// <summary>
        /// Gets or sets the maximum number of days to store the image.
        /// </summary>
        public int MaxDays { get; set; }
        /// <summary>
        /// Gets or sets the maximum number of days to cache the image in the browser.
        /// </summary>
        public int BrowserMaxDays { get; set; }
        /// <summary>
        /// Gets or sets the maximum number folder levels to nest the cached images.
        /// </summary>
        public int FolderDepth { get; set; }
        /// <summary>
        /// Gets or sets a value indicating whether to periodically trim the cache.
        /// </summary>
        public bool TrimCache { get; set; }
        /// <summary>
        /// Gets a value indicating whether the image is new or updated in an asynchronous manner.
        /// </summary>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public abstract Task<bool> IsNewOrUpdatedAsync();
        /// <summary>
        /// Adds the image to the cache in an asynchronous manner.
        /// </summary>
        /// <param name="stream">
        /// The stream containing the image data.
        /// </param>
        /// <param name="contentType">
        /// The content type of the image.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/> representing an asynchronous operation.
        /// </returns>
        public abstract Task AddImageToCacheAsync(Stream stream, string contentType);
        /// <summary>
        /// Trims the cache of any expired items in an asynchronous manner.
        /// Call <see cref="M:DebounceTrimmerAsync"/> within your implementation to correctly debounce cache cleanup.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> representing an asynchronous operation.
        /// </returns>
        public abstract Task TrimCacheAsync();
        /// <summary>
        /// Gets a string identifying the cached file name.
        /// </summary>
        /// <returns>
        /// The asynchronous <see cref="Task"/> returning the value.
        /// </returns>
        public virtual async Task<string> CreateCachedFileNameAsync()
        {
            return await Task.FromResult(CachedImageHelper.GetCachedImageFileName(this.FullPath, this.Querystring));
        }
        /// <summary>
        /// Rewrites the path to point to the cached image.
        /// </summary>
        /// <param name="context">
        /// The <see cref="HttpContext"/> encapsulating all information about the request.
        /// </param>
        public abstract void RewritePath(HttpContext context);
        /// <summary>
        /// Gets a value indicating whether the given images creation date is out with
        /// the prescribed limit.
        /// </summary>
        /// <param name="creationDate">The creation date.</param>
        /// <returns>
        /// The true if the date is out with the limit, otherwise; false.
        /// </returns>
        protected virtual bool IsExpired(DateTime creationDate)
        {
            return creationDate < DateTime.UtcNow.AddDays(-this.MaxDays);
        }
        /// <summary>
        /// Provides a means to augment the cache settings taken from the configuration in derived classes.
        /// This allows for configuration of cache objects outside the normal configuration files, for example
        /// by using app settings in the Azure platform.
        /// </summary>
        /// <param name="settings">The current settings.</param>
        protected virtual void AugmentSettings(Dictionary<string, string> settings)
        {
        }
        /// <summary>
        /// Will schedule any cache trimming  to ensure that only one cleanup operation is running at any one time
        /// and that it is a quiet time to do so.
        /// </summary>
        /// <param name="trimmer">The cache trimming method.</param>
        protected void ScheduleCacheTrimmer(Func<CancellationToken, Task> trimmer)
        {
            Trimmer.ScheduleTrimCache(trimmer);
        }
        /// <summary>
        /// Provides an entry point to augmentation of the <see cref="Settings"/> dictionary
        /// </summary>
        /// <param name="settings">Dictionary of settings</param>
        /// <returns>augmented dictionary of settings</returns>
        private Dictionary<string, string> AugmentSettingsCore(Dictionary<string, string> settings)
        {
            this.AugmentSettings(settings);
            return settings;
        }
        /// <summary>
        /// This ensures that any cache trimming operation is executed on a background thread and that only one operation can ever occur at one time.
        /// The execution will occur on a sliding timeframe, so anytime ScheduleTrimCache is called, it will check if it's within the timeout, if not it
        /// will delay the timeout again but only until the maximum wait time is reached.
        /// </summary>
        private class CacheTrimmer : IRegisteredObject
        {
            /// <summary>
            /// The object to lock against
            /// </summary>
            private static readonly object Locker = new object();
            /// <summary>
            /// Whether the trimming task is running
            /// </summary>
            private static bool trim;
            /// <summary>
            /// The asynchronous trimmer task
            /// </summary>
            // private static Task task;
            /// <summary>
            /// The cancellation token source
            /// </summary>
            private static readonly CancellationTokenSource tokenSource = new CancellationTokenSource();
            /// <summary>
            /// The timestamp
            /// </summary>
            private DateTime timestamp;
            /// <summary>
            /// The timer
            /// </summary>
            private Timer timer;
            /// <summary>
            /// Initializes a new instance of the <see cref="CacheTrimmer"/> class.
            /// </summary>
            public CacheTrimmer()
            {
                HostingEnvironment.RegisterObject(this);
            }
            /// <summary>
            /// The sliding delay time
            /// </summary>
            private const int WaitMilliseconds = 120000; //(2 min)
            /// <summary>
            /// The maximum time period that will elapse until we must trim (30 mins)
            /// </summary>
            private const int MaxWaitMilliseconds = 1800000;
            public void ScheduleTrimCache(Func<CancellationToken, Task> trimmer)
            {
                // Don't continue if already trimming or canceled
                if (trim || tokenSource.IsCancellationRequested)
                {
                    return;
                }
                lock (Locker)
                {
                    if (timer == null)
                    {
                        // It's the initial call to this at the beginning or after successful commit
                        timestamp = DateTime.Now;
                        timer = new Timer(_ => TimerRelease(trimmer));
                        timer.Change(WaitMilliseconds, 0);
                    }
                    else
                    {
                        // If we've been cancelled then be sure to cancel the timer
                        if (tokenSource.IsCancellationRequested)
                        {
                            // Stop the timer
                            timer.Change(Timeout.Infinite, Timeout.Infinite);
                            timer.Dispose();
                            timer = null;
                        }
                        else if (
                            // Must be less than the max and less than the delay
                            DateTime.Now - timestamp < TimeSpan.FromMilliseconds(MaxWaitMilliseconds) &&
                            DateTime.Now - timestamp < TimeSpan.FromMilliseconds(WaitMilliseconds))
                        {
                            // Delay
                            timer.Change(WaitMilliseconds, 0);
                        }
                        else
                        {
                            // Cannot delay! the callback will execute on the pending timeout
                        }
                    }
                }
            }
            /// <summary>
            /// Performs the trimming function.
            /// </summary>
            /// <param name="trimmer">The trimmer method.</param>
            /// <returns></returns>
            private static async Task PerformTrim(Func<CancellationToken, Task> trimmer)
            {
                if (tokenSource.IsCancellationRequested)
                {
                    return;
                }
                trim = true;
                await trimmer(tokenSource.Token);
                trim = false;
            }
            /// <summary>
            /// Releases the timer operation, running the cache trimmer.
            /// </summary>
            /// <param name="trimmer">The trimmer method.</param>
            private void TimerRelease(Func<CancellationToken, Task> trimmer)
            {
                lock (Locker)
                {
                    // Don't continue if already trimming or canceled
                    if (trim || tokenSource.IsCancellationRequested)
                    {
                        return;
                    }
                    // If the timer is not null then a trim has been scheduled
                    if (timer != null)
                    {
                        // Stop the timer
                        timer.Change(Timeout.Infinite, Timeout.Infinite);
                        timer.Dispose();
                        timer = null;
                        // Trim!
                        trim = true;
                        // We are already on a background then so we will block here
                        PerformTrim(trimmer).Wait();
                    }
                }
            }
            /// <inheritdoc />
            public void Stop(bool immediate)
            {
                if (timer != null)
                {
                    timer.Change(Timeout.Infinite, Timeout.Infinite);
                    timer.Dispose();
                    timer = null;
                }
                if (!tokenSource.IsCancellationRequested)
                {
                    tokenSource.Cancel();
                }
            }
        }
    }
}

IImageCacheExtended.cs

IImageCacheExtended.cs
Copy Code
namespace Devesprit.ImageServer.Caching
{
    /// <summary>
    /// An extended image cache with additional configuration options.
    /// </summary>
    public interface IImageCacheExtended : IImageCache
    {
        /// <summary>
        /// Gets or sets the maximum number folder levels to nest the cached images.
        /// </summary>
        int FolderDepth { get; set; }
        /// <summary>
        /// Gets or sets a value indicating whether to periodically trim the cache.
        /// </summary>
        bool TrimCache { get; set; }
    }
}

How to install

To install your caching system please add a section in Cache.Config and put your assembly in Bin directory.

See Also

Configuration