diff --git a/src/ImageProcessor.Web/Caching/Cacher.cs b/src/ImageProcessor.Web/Caching/Cacher.cs
new file mode 100644
index 000000000..a4a6f67af
--- /dev/null
+++ b/src/ImageProcessor.Web/Caching/Cacher.cs
@@ -0,0 +1,389 @@
+
+
+namespace ImageProcessor.Web.Caching
+{
+ #region Using
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.IO;
+ using System.Text.RegularExpressions;
+ using System.Threading.Tasks;
+ using System.Web;
+ using System.Web.Hosting;
+ using ImageProcessor.Helpers.Extensions;
+ using ImageProcessor.Web.Config;
+ #endregion
+
+ ///
+ /// The cache.
+ ///
+ internal sealed class Cache
+ {
+ #region Fields
+
+ ///
+ /// The maximum number of days to cache files on the system for.
+ ///
+ internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays;
+
+ ///
+ /// The valid sub directory chars. This used in combination with the file limit per folder
+ /// allows the storage of 360,000 image files in the cache.
+ ///
+ private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+
+ ///
+ /// The maximum number of files allowed in the directory.
+ ///
+ ///
+ /// NTFS directories can handle up to 10,000 files in the directory before slowing down.
+ /// This will help us to ensure that don't go over that limit.
+ ///
+ ///
+ ///
+ ///
+ private const int MaxFilesCount = 10000;
+
+ ///
+ /// The regular expression to search strings for file extensions.
+ ///
+ private static readonly Regex FormatRegex = new Regex(
+ @"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled);
+
+ ///
+ /// The regular expression to search strings for valid subfolder names.
+ /// We're specifically not using a shorter regex as we need to be able to iterate through
+ /// each match group.
+ ///
+ private static readonly Regex SubFolderRegex =
+ new Regex(
+ @"(\/(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9)\/)",
+ RegexOptions.Compiled);
+
+ ///
+ /// The absolute path to virtual cache path on the server.
+ ///
+ private static readonly string AbsoluteCachePath =
+ HostingEnvironment.MapPath(ImageProcessorConfig.Instance.VirtualCachePath);
+
+ ///
+ /// The concurrent dictionary.
+ ///
+ private ConcurrentDictionary concurrentDictionary =
+ new ConcurrentDictionary();
+
+ #endregion
+
+ #region Methods
+
+ public void Test()
+ {
+ Task task = this.CreateDirectoriesAsync();
+ if (task.Result)
+ {
+
+ }
+
+ }
+
+ #region Internal
+ ///
+ /// Converts an absolute file path
+ ///
+ /// The absolute path to convert.
+ /// The from the current context.
+ /// The virtual path to the file.
+ internal string GetVirtualPath(string absolutePath, HttpRequest request)
+ {
+ string applicationPath = request.PhysicalApplicationPath;
+ string virtualDir = request.ApplicationPath;
+ virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
+
+ if (applicationPath != null)
+ {
+ return absolutePath.Replace(applicationPath, virtualDir).Replace(@"\", "/");
+ }
+
+ throw new InvalidOperationException(
+ "We can only map an absolute back to a relative path if the application path is available.");
+ }
+
+ ///
+ /// Gets the full transformed cached path for the image.
+ /// The file names are stored as MD5 encrypted versions of the full request path.
+ /// This should make them unique enough to
+ ///
+ /// The original image path.
+ /// The original image name.
+ /// The full cached path for the image.
+ internal string GetCachePath(string imagePath, string imageName)
+ {
+ string cachedPath = string.Empty;
+
+ if (AbsoluteCachePath != null)
+ {
+ // Use an md5 hash of the full path including the querystring to create the image name.
+ // That name can also be used as a key for the cached image and we should be able to use
+ // The first character of that hash as a subfolder.
+ string parsedExtension = this.ParseExtension(imagePath);
+ string fallbackExtension = imageName.Substring(imageName.LastIndexOf(".", StringComparison.Ordinal) + 1);
+ string encryptedName = imagePath.ToMD5Fingerprint();
+ string subpath = encryptedName.Substring(0, 1);
+
+ string cachedFileName = string.Format(
+ "{0}.{1}",
+ encryptedName,
+ !string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension : fallbackExtension);
+
+ cachedPath = Path.Combine(AbsoluteCachePath, subpath, cachedFileName);
+ }
+
+ return cachedPath;
+ }
+
+ ///
+ /// Creates the cache directories for storing images.
+ ///
+ ///
+ /// The true if the cache directories are created successfully; otherwise, false.
+ ///
+ internal /*async*/ Task CreateDirectoriesAsync()
+ {
+ return this.CreateDirectoriesAsyncTasks().ToTask();
+ }
+
+ ///
+ /// Adds an image to the cache.
+ ///
+ ///
+ /// The cached path.
+ ///
+ ///
+ /// The last write time.
+ ///
+ ///
+ /// The task.
+ ///
+ internal Task /*async*/ AddImageToCacheAsync(string cachedPath, DateTime lastWriteTimeUtc)
+ {
+ return this.AddImageToCacheAsyncTask(cachedPath, lastWriteTimeUtc).ToTask();
+ }
+
+ ///
+ /// Returns a value indicating whether the original file has been updated.
+ ///
+ /// The original image path.
+ /// The cached image path.
+ /// Whether the file is a remote request.
+ ///
+ /// True if the the original file has been updated; otherwise, false.
+ ///
+ internal /*async*/ Task IsUpdatedFileAsync(string imagePath, string cachedImagePath, bool isRemote)
+ {
+ return this.IsUpdatedFileAsyncTask(imagePath, cachedImagePath, isRemote).ToTask();
+ }
+
+ ///
+ /// Sets the LastWriteTime of the cached file to match the original file.
+ ///
+ ///
+ /// The original image path.
+ ///
+ ///
+ /// The cached image path.
+ ///
+ /// Whether the file is remote.
+ ///
+ /// The set to the last write time of the file.
+ ///
+ internal /*async*/ Task SetCachedLastWriteTimeAsync(string imagePath, string cachedImagePath, bool isRemote)
+ {
+ return this.SetCachedLastWriteTimeAsyncTask(imagePath, cachedImagePath, isRemote).ToTask();
+ }
+
+ #endregion
+
+ #region Private
+ ///
+ /// The create directories async tasks.
+ ///
+ ///
+ /// The .
+ ///
+ private IEnumerable CreateDirectoriesAsyncTasks()
+ {
+ bool success = true;
+
+ try
+ {
+ Parallel.ForEach(
+ ValidSubDirectoryChars.ToCharArray(),
+ (extension, loop) =>
+ {
+ string path = Path.Combine(AbsoluteCachePath, extension.ToString(CultureInfo.InvariantCulture));
+ DirectoryInfo directoryInfo = new DirectoryInfo(path);
+
+ if (!directoryInfo.Exists)
+ {
+ directoryInfo.Create();
+ }
+ });
+ }
+ catch
+ {
+ success = false;
+ }
+
+ yield return TaskEx.FromResult(success);
+ }
+
+ ///
+ /// Adds an image to the cache.
+ ///
+ ///
+ /// The cached path.
+ ///
+ ///
+ /// The last write time.
+ ///
+ ///
+ /// The .
+ ///
+ private IEnumerable AddImageToCacheAsyncTask(string cachedPath, DateTime lastWriteTimeUtc)
+ {
+ string key = Path.GetFileNameWithoutExtension(cachedPath);
+ DateTime expires = DateTime.UtcNow.AddDays(MaxFileCachedDuration).ToUniversalTime();
+ CachedImage cachedImage = new CachedImage(cachedPath, MaxFileCachedDuration, lastWriteTimeUtc, expires);
+ PersistantDictionary.Instance.Add(key, cachedImage);
+
+ yield break;
+ }
+
+ ///
+ /// Returns a value indicating whether the original file has been updated.
+ ///
+ /// The original image path.
+ /// The cached image path.
+ /// Whether the file is a remote request.
+ ///
+ /// The .
+ ///
+ private IEnumerable IsUpdatedFileAsyncTask(string imagePath, string cachedImagePath, bool isRemote)
+ {
+ string key = Path.GetFileNameWithoutExtension(cachedImagePath);
+ CachedImage cachedImage;
+ bool isUpdated = false;
+
+ if (isRemote)
+ {
+ if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
+ {
+ // Can't check the last write time so check to see if the cached image is set to expire
+ // or if the max age is different.
+ if (cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)
+ || cachedImage.MaxAge != MaxFileCachedDuration)
+ {
+ if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
+ {
+ isUpdated = true;
+ }
+ }
+ }
+ else
+ {
+ // Nothing in the cache so we should return true.
+ isUpdated = true;
+ }
+ }
+
+ // Test now for locally requested files.
+ if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
+ {
+ FileInfo imageFileInfo = new FileInfo(imagePath);
+
+ if (imageFileInfo.Exists)
+ {
+ // Check to see if the last write time is different of whether the
+ // cached image is set to expire or if the max age is different.
+ if (imageFileInfo.LastWriteTimeUtc != cachedImage.LastWriteTimeUtc
+ || cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)
+ || cachedImage.MaxAge != MaxFileCachedDuration)
+ {
+ if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
+ {
+ isUpdated = true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // Nothing in the cache so we should return true.
+ isUpdated = true;
+ }
+
+ yield return TaskEx.FromResult(isUpdated);
+ }
+
+ ///
+ /// Sets the LastWriteTime of the cached file to match the original file.
+ ///
+ ///
+ /// The original image path.
+ ///
+ ///
+ /// The cached image path.
+ ///
+ /// Whether the file is remote.
+ ///
+ /// The .
+ ///
+ private IEnumerable SetCachedLastWriteTimeAsyncTask(string imagePath, string cachedImagePath, bool isRemote)
+ {
+ FileInfo cachedFileInfo = new FileInfo(cachedImagePath);
+ DateTime lastWriteTime = DateTime.MinValue.ToUniversalTime();
+
+ if (isRemote)
+ {
+ if (cachedFileInfo.Exists)
+ {
+ lastWriteTime = cachedFileInfo.LastWriteTimeUtc;
+ }
+ }
+ else
+ {
+ FileInfo imageFileInfo = new FileInfo(imagePath);
+
+ if (imageFileInfo.Exists && cachedFileInfo.Exists)
+ {
+ DateTime dateTime = imageFileInfo.LastWriteTimeUtc;
+ cachedFileInfo.LastWriteTimeUtc = dateTime;
+
+ lastWriteTime = dateTime;
+ }
+ }
+
+ yield return TaskEx.FromResult(lastWriteTime);
+ }
+
+ ///
+ /// Returns the correct file extension for the given string input
+ ///
+ ///
+ /// The string to parse.
+ ///
+ ///
+ /// The correct file extension for the given string input if it can find one; otherwise an empty string.
+ ///
+ private string ParseExtension(string input)
+ {
+ Match match = FormatRegex.Match(input);
+
+ return match.Success ? match.Value : string.Empty;
+ }
+ #endregion
+ #endregion
+ }
+}
diff --git a/src/ImageProcessor.Web/Caching/Copy of DiskCache.cs b/src/ImageProcessor.Web/Caching/Copy of DiskCache.cs
new file mode 100644
index 000000000..84fc204e3
--- /dev/null
+++ b/src/ImageProcessor.Web/Caching/Copy of DiskCache.cs
@@ -0,0 +1,346 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) James South.
+// Dual licensed under the MIT or GPL Version 2 licenses.
+//
+// -----------------------------------------------------------------------
+
+namespace ImageProcessor.Web.Caching
+{
+ #region Using
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using System.Threading.Tasks;
+ using System.Web;
+ using System.Web.Hosting;
+ using ImageProcessor.Helpers.Extensions;
+ using ImageProcessor.Web.Config;
+ #endregion
+
+ ///
+ /// Encapsulates methods to handle disk caching of images.
+ ///
+ internal sealed class DiskCache
+ {
+ #region Fields
+ ///
+ /// The maximum number of days to cache files on the system for.
+ ///
+ internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays;
+
+ ///
+ /// The valid sub directory chars. This used in combination with the file limit per folder
+ /// allows the storage of 360,000 image files in the cache.
+ ///
+ private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+
+ ///
+ /// The maximum number of files allowed in the directory.
+ ///
+ ///
+ /// NTFS directories can handle up to 10,000 files in the directory before slowing down.
+ /// This will help us to ensure that don't go over that limit.
+ ///
+ ///
+ ///
+ ///
+ private const int MaxFilesCount = 10000;
+
+ ///
+ /// The regular expression to search strings for file extensions.
+ ///
+ private static readonly Regex FormatRegex = new Regex(
+ @"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled);
+
+ ///
+ /// The regular expression to search strings for valid subfolder names.
+ /// We're specifically not using a shorter regex as we need to be able to iterate through
+ /// each match group.
+ ///
+ private static readonly Regex SubFolderRegex = new Regex(@"(\/(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9)\/)", RegexOptions.Compiled);
+
+ ///
+ /// The absolute path to virtual cache path on the server.
+ ///
+ private static readonly string AbsoluteCachePath =
+ HostingEnvironment.MapPath(ImageProcessorConfig.Instance.VirtualCachePath);
+
+ #endregion
+
+ #region Methods
+ ///
+ /// The create cache paths.
+ ///
+ ///
+ /// The true if the cache directories are created successfully; otherwise, false.
+ ///
+ internal static bool CreateCacheDirectories()
+ {
+ try
+ {
+ Parallel.ForEach(
+ ValidSubDirectoryChars.ToCharArray(),
+ (extension, loop) =>
+ {
+ string path = Path.Combine(AbsoluteCachePath, extension.ToString(CultureInfo.InvariantCulture));
+ DirectoryInfo directoryInfo = new DirectoryInfo(path);
+
+ if (!directoryInfo.Exists)
+ {
+ directoryInfo.Create();
+ }
+ });
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets the full transformed cached path for the image.
+ /// The file names are stored as MD5 encrypted versions of the full request path.
+ /// This should make them unique enough to
+ ///
+ /// The original image path.
+ /// The original image name.
+ /// The full cached path for the image.
+ internal static string GetCachePath(string imagePath, string imageName)
+ {
+ string cachedPath = string.Empty;
+
+ if (AbsoluteCachePath != null)
+ {
+ // Use an md5 hash of the full path including the querystring to create the image name.
+ // That name can also be used as a key for the cached image and we should be able to use
+ // The first character of that hash as a subfolder.
+ string parsedExtension = ParseExtension(imagePath);
+ string fallbackExtension = imageName.Substring(imageName.LastIndexOf(".", StringComparison.Ordinal) + 1);
+ string encryptedName = imagePath.ToMD5Fingerprint();
+ string subpath = encryptedName.Substring(0, 1);
+
+ string cachedFileName = string.Format(
+ "{0}.{1}",
+ encryptedName,
+ !string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension : fallbackExtension);
+
+ cachedPath = Path.Combine(AbsoluteCachePath, subpath, cachedFileName);
+ }
+
+ return cachedPath;
+ }
+
+ ///
+ /// Adds an image to the cache.
+ ///
+ ///
+ /// The cached path.
+ ///
+ ///
+ /// The last write time.
+ ///
+ internal static void AddImageToCache(string cachedPath, DateTime lastWriteTimeUtc)
+ {
+ string key = Path.GetFileNameWithoutExtension(cachedPath);
+ DateTime expires = DateTime.UtcNow.AddDays(MaxFileCachedDuration).ToUniversalTime();
+ CachedImage cachedImage = new CachedImage(cachedPath, MaxFileCachedDuration, lastWriteTimeUtc, expires);
+ PersistantDictionary.Instance.Add(key, cachedImage);
+ }
+
+ ///
+ /// Converts an absolute file path
+ ///
+ /// The absolute path to convert.
+ /// The from the current context.
+ /// The virtual path to the file.
+ internal static string GetVirtualPath(string absolutePath, HttpRequest request)
+ {
+ string applicationPath = request.PhysicalApplicationPath;
+ string virtualDir = request.ApplicationPath;
+ virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
+
+ if (applicationPath != null)
+ {
+ return absolutePath.Replace(applicationPath, virtualDir).Replace(@"\", "/");
+ }
+
+ throw new InvalidOperationException("We can only map an absolute back to a relative path if the application path is available.");
+ }
+
+ ///
+ /// Returns a value indicating whether the original file has been updated.
+ ///
+ /// The original image path.
+ /// The cached image path.
+ /// Whether the file is a remote request.
+ ///
+ /// True if the the original file has been updated; otherwise, false.
+ ///
+ internal static bool IsUpdatedFile(string imagePath, string cachedImagePath, bool isRemote)
+ {
+ string key = Path.GetFileNameWithoutExtension(cachedImagePath);
+ CachedImage cachedImage;
+
+ if (isRemote)
+ {
+ if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
+ {
+ // Can't check the last write time so check to see if the cached image is set to expire
+ // or if the max age is different.
+ if (cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)
+ || cachedImage.MaxAge != MaxFileCachedDuration)
+ {
+ if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
+ {
+ // We can jump out here.
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Nothing in the cache so we should return true.
+ return true;
+ }
+
+ // Test now for locally requested files.
+ if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
+ {
+ FileInfo imageFileInfo = new FileInfo(imagePath);
+
+ if (imageFileInfo.Exists)
+ {
+ // Check to see if the last write time is different of whether the
+ // cached image is set to expire or if the max age is different.
+ if (imageFileInfo.LastWriteTimeUtc != cachedImage.LastWriteTimeUtc
+ || cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)
+ || cachedImage.MaxAge != MaxFileCachedDuration)
+ {
+ if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // Nothing in the cache so we should return true.
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Sets the LastWriteTime of the cached file to match the original file.
+ ///
+ ///
+ /// The original image path.
+ ///
+ ///
+ /// The cached image path.
+ ///
+ /// Whether the file is remote.
+ ///
+ /// The set to the last write time of the file.
+ ///
+ internal static DateTime SetCachedLastWriteTime(string imagePath, string cachedImagePath, bool isRemote)
+ {
+ FileInfo cachedFileInfo = new FileInfo(cachedImagePath);
+
+ if (isRemote)
+ {
+ if (cachedFileInfo.Exists)
+ {
+ return cachedFileInfo.LastWriteTimeUtc;
+ }
+ }
+
+ FileInfo imageFileInfo = new FileInfo(imagePath);
+
+ if (imageFileInfo.Exists && cachedFileInfo.Exists)
+ {
+ DateTime dateTime = imageFileInfo.LastWriteTimeUtc;
+ cachedFileInfo.LastWriteTimeUtc = dateTime;
+
+ return dateTime;
+ }
+
+ return DateTime.MinValue.ToUniversalTime();
+ }
+
+ ///
+ /// Purges any files from the file-system cache in the given folders.
+ ///
+ internal static void TrimCachedFolders()
+ {
+ // Group each cache folder and clear any expired items or any that exeed
+ // the maximum allowable count.
+ var groups = PersistantDictionary.Instance.ToList()
+ .GroupBy(x => SubFolderRegex.Match(x.Value.Path).Value)
+ .Where(g => g.Count() > MaxFilesCount);
+
+ foreach (var group in groups)
+ {
+ int groupCount = group.Count();
+
+ foreach (KeyValuePair pair in group.OrderBy(x => x.Value.ExpiresUtc))
+ {
+ // If the group count is equal to the max count minus 1 then we know we
+ // are counting down from a full directory not simply clearing out
+ // expired items.
+ if (groupCount == MaxFilesCount - 1)
+ {
+ break;
+ }
+
+ try
+ {
+ // Remove from the cache and delete each CachedImage.
+ FileInfo fileInfo = new FileInfo(pair.Value.Path);
+ string key = Path.GetFileNameWithoutExtension(fileInfo.Name);
+ CachedImage cachedImage;
+
+ if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
+ {
+ fileInfo.Delete();
+ groupCount -= 1;
+ }
+ }
+ catch (Exception)
+ {
+ // Do Nothing, skip to the next.
+ // TODO: Should we handle this?
+ continue;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Returns the correct file extension for the given string input
+ ///
+ ///
+ /// The string to parse.
+ ///
+ ///
+ /// The correct file extension for the given string input if it can find one; otherwise an empty string.
+ ///
+ private static string ParseExtension(string input)
+ {
+ Match match = FormatRegex.Match(input);
+
+ return match.Success ? match.Value : string.Empty;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/ImageProcessor.Web/Helpers/TaskEx.cs b/src/ImageProcessor.Web/Helpers/TaskEx.cs
new file mode 100644
index 000000000..6d85c45c1
--- /dev/null
+++ b/src/ImageProcessor.Web/Helpers/TaskEx.cs
@@ -0,0 +1,174 @@
+// License: CPOL at http://www.codeproject.com/info/cpol10.aspx
+
+
+namespace System.Threading.Tasks
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Extensions related to the classes.
+ /// Supports implementing "async"-style methods in C#4 using iterators.
+ ///
+ ///
+ /// I would call this TaskExtensions, except that clients must name the class to use methods like .
+ /// Based on work from Await Tasks in C#4 using Iterators by Keith L Robertson.
+ ///
+ ///
+ public static class TaskEx
+ {
+ ///
+ /// Return a Completed with a specific value.
+ ///
+ ///
+ /// The result
+ ///
+ ///
+ /// The result Value.
+ ///
+ ///
+ /// The .
+ ///
+ public static Task FromResult(TResult resultValue)
+ {
+ var completionSource = new TaskCompletionSource();
+ completionSource.SetResult(resultValue);
+ return completionSource.Task;
+ }
+
+ ///
+ /// Transform an enumeration of into a single non-Result .
+ ///
+ ///
+ /// The tasks.
+ ///
+ ///
+ /// The .
+ ///
+ public static Task ToTask(this IEnumerable tasks)
+ {
+ return ToTask(tasks);
+ }
+
+ ///
+ /// Transform an enumeration of into a single .
+ /// The final in must be a .
+ ///
+ ///
+ /// The task results
+ ///
+ ///
+ /// The tasks.
+ ///
+ ///
+ /// The .
+ ///
+ public static Task ToTask(this IEnumerable tasks)
+ {
+ var taskScheduler =
+ SynchronizationContext.Current == null
+ ? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext();
+ var taskEnumerator = tasks.GetEnumerator();
+ var completionSource = new TaskCompletionSource();
+
+ // Clean up the enumerator when the task completes.
+ completionSource.Task.ContinueWith(t => taskEnumerator.Dispose(), taskScheduler);
+
+ ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, null);
+ return completionSource.Task;
+ }
+
+ ///
+ /// If the previous task Canceled or Faulted, complete the master task with the same .
+ /// Obtain the next from the .
+ /// If none, complete the master task, possibly with the of the last task.
+ /// Otherwise, set up the task with a continuation to come do this again when it completes.
+ ///
+ private static void ToTaskDoOneStep(
+ IEnumerator taskEnumerator, TaskScheduler taskScheduler,
+ TaskCompletionSource completionSource, Task completedTask)
+ {
+ // Check status of previous nested task (if any), and stop if Canceled or Faulted.
+ TaskStatus status;
+ if (completedTask == null)
+ {
+ // This is the first task from the iterator; skip status check.
+ }
+ else if ((status = completedTask.Status) == TaskStatus.Canceled)
+ {
+ completionSource.SetCanceled();
+ return;
+ }
+ else if (status == TaskStatus.Faulted)
+ {
+ completionSource.SetException(completedTask.Exception);
+ return;
+ }
+
+ // Check for cancellation before looking for the next task.
+ // This causes a problem where the ultimate Task does not complete and fire any continuations; I don't know why.
+ // So cancellation from the Token must be handled within the iterator itself.
+ //if (cancellationToken.IsCancellationRequested) {
+ // completionSource.SetCanceled();
+ // return;
+ //}
+
+ // Find the next Task in the iterator; handle cancellation and other exceptions.
+ Boolean haveMore;
+ try
+ {
+ haveMore = taskEnumerator.MoveNext();
+
+ }
+ catch (OperationCanceledException cancExc)
+ {
+ //if (cancExc.CancellationToken == cancellationToken) completionSource.SetCanceled();
+ //else completionSource.SetException(cancExc);
+ completionSource.SetCanceled();
+ return;
+ }
+ catch (Exception exc)
+ {
+ completionSource.SetException(exc);
+ return;
+ }
+
+ if (!haveMore)
+ {
+ // No more tasks; set the result from the last completed task (if any, unless no result is requested).
+ // We know it's not Canceled or Faulted because we checked at the start of this method.
+ if (typeof(TResult) == typeof(VoidResult))
+ { // No result
+ completionSource.SetResult(default(TResult));
+ }
+ else if (!(completedTask is Task))
+ { // Wrong result
+ completionSource.SetException(new InvalidOperationException(
+ "Asynchronous iterator " + taskEnumerator +
+ " requires a final result task of type " + typeof(Task).FullName +
+ (completedTask == null ? ", but none was provided." :
+ "; the actual task type was " + completedTask.GetType().FullName)));
+ }
+ else
+ {
+ completionSource.SetResult(((Task)completedTask).Result);
+ }
+ }
+ else
+ {
+ // When the nested task completes, continue by performing this function again.
+ // Note: This is NOT a recursive call; the current method activation will complete
+ // almost immediately and independently of the lambda continuation.
+ taskEnumerator.Current.ContinueWith(
+ nextTask => ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, nextTask),
+ taskScheduler);
+ }
+ }
+
+ ///
+ /// Internal marker type for using to implement .
+ ///
+ private abstract class VoidResult
+ {
+ }
+ }
+}
diff --git a/src/ImageProcessor.Web/ImageProcessor.Web.csproj b/src/ImageProcessor.Web/ImageProcessor.Web.csproj
index 0ab6f04c4..42983cf84 100644
--- a/src/ImageProcessor.Web/ImageProcessor.Web.csproj
+++ b/src/ImageProcessor.Web/ImageProcessor.Web.csproj
@@ -87,7 +87,9 @@
+
+