Browse Source

First attempt at making it Async

Former-commit-id: 3a70d382e62de8e4f8a48d7a991c5815840ff72f
af/merge-core
JimBobSquarePants 13 years ago
parent
commit
92ff77dc5a
  1. 389
      src/ImageProcessor.Web/Caching/Cacher.cs
  2. 346
      src/ImageProcessor.Web/Caching/Copy of DiskCache.cs
  3. 174
      src/ImageProcessor.Web/Helpers/TaskEx.cs
  4. 2
      src/ImageProcessor.Web/ImageProcessor.Web.csproj

389
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
/// <summary>
/// The cache.
/// </summary>
internal sealed class Cache
{
#region Fields
/// <summary>
/// The maximum number of days to cache files on the system for.
/// </summary>
internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays;
/// <summary>
/// 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.
/// </summary>
private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789";
/// <summary>
/// The maximum number of files allowed in the directory.
/// </summary>
/// <remarks>
/// 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.
/// <see cref="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
/// <see cref="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
/// <see cref="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
/// </remarks>
private const int MaxFilesCount = 10000;
/// <summary>
/// The regular expression to search strings for file extensions.
/// </summary>
private static readonly Regex FormatRegex = new Regex(
@"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled);
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// The absolute path to virtual cache path on the server.
/// </summary>
private static readonly string AbsoluteCachePath =
HostingEnvironment.MapPath(ImageProcessorConfig.Instance.VirtualCachePath);
/// <summary>
/// The concurrent dictionary.
/// </summary>
private ConcurrentDictionary<string, CachedImage> concurrentDictionary =
new ConcurrentDictionary<string, CachedImage>();
#endregion
#region Methods
public void Test()
{
Task<bool> task = this.CreateDirectoriesAsync();
if (task.Result)
{
}
}
#region Internal
/// <summary>
/// Converts an absolute file path
/// </summary>
/// <param name="absolutePath">The absolute path to convert.</param>
/// <param name="request">The <see cref="T:System.Web.HttpRequest"/>from the current context.</param>
/// <returns>The virtual path to the file.</returns>
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.");
}
/// <summary>
/// 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
/// </summary>
/// <param name="imagePath">The original image path.</param>
/// <param name="imageName">The original image name.</param>
/// <returns>The full cached path for the image.</returns>
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;
}
/// <summary>
/// Creates the cache directories for storing images.
/// </summary>
/// <returns>
/// The true if the cache directories are created successfully; otherwise, false.
/// </returns>
internal /*async*/ Task<bool> CreateDirectoriesAsync()
{
return this.CreateDirectoriesAsyncTasks().ToTask<bool>();
}
/// <summary>
/// Adds an image to the cache.
/// </summary>
/// <param name="cachedPath">
/// The cached path.
/// </param>
/// <param name="lastWriteTimeUtc">
/// The last write time.
/// </param>
/// <returns>
/// The task.
/// </returns>
internal Task /*async*/ AddImageToCacheAsync(string cachedPath, DateTime lastWriteTimeUtc)
{
return this.AddImageToCacheAsyncTask(cachedPath, lastWriteTimeUtc).ToTask();
}
/// <summary>
/// Returns a value indicating whether the original file has been updated.
/// </summary>
/// <param name="imagePath">The original image path.</param>
/// <param name="cachedImagePath">The cached image path.</param>
/// <param name="isRemote">Whether the file is a remote request.</param>
/// <returns>
/// True if the the original file has been updated; otherwise, false.
/// </returns>
internal /*async*/ Task<bool> IsUpdatedFileAsync(string imagePath, string cachedImagePath, bool isRemote)
{
return this.IsUpdatedFileAsyncTask(imagePath, cachedImagePath, isRemote).ToTask<bool>();
}
/// <summary>
/// Sets the LastWriteTime of the cached file to match the original file.
/// </summary>
/// <param name="imagePath">
/// The original image path.
/// </param>
/// <param name="cachedImagePath">
/// The cached image path.
/// </param>
/// <param name="isRemote">Whether the file is remote.</param>
/// <returns>
/// The <see cref="System.DateTime"/> set to the last write time of the file.
/// </returns>
internal /*async*/ Task<DateTime> SetCachedLastWriteTimeAsync(string imagePath, string cachedImagePath, bool isRemote)
{
return this.SetCachedLastWriteTimeAsyncTask(imagePath, cachedImagePath, isRemote).ToTask<DateTime>();
}
#endregion
#region Private
/// <summary>
/// The create directories async tasks.
/// </summary>
/// <returns>
/// The <see cref="IEnumerable{Task}"/>.
/// </returns>
private IEnumerable<Task> 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);
}
/// <summary>
/// Adds an image to the cache.
/// </summary>
/// <param name="cachedPath">
/// The cached path.
/// </param>
/// <param name="lastWriteTimeUtc">
/// The last write time.
/// </param>
/// <returns>
/// The <see cref="IEnumerable{Task}"/>.
/// </returns>
private IEnumerable<Task> 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;
}
/// <summary>
/// Returns a value indicating whether the original file has been updated.
/// </summary>
/// <param name="imagePath">The original image path.</param>
/// <param name="cachedImagePath">The cached image path.</param>
/// <param name="isRemote">Whether the file is a remote request.</param>
/// <returns>
/// The <see cref="IEnumerable{Task}"/>.
/// </returns>
private IEnumerable<Task> 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);
}
/// <summary>
/// Sets the LastWriteTime of the cached file to match the original file.
/// </summary>
/// <param name="imagePath">
/// The original image path.
/// </param>
/// <param name="cachedImagePath">
/// The cached image path.
/// </param>
/// <param name="isRemote">Whether the file is remote.</param>
/// <returns>
/// The <see cref="IEnumerable{Task}"/>.
/// </returns>
private IEnumerable<Task> 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);
}
/// <summary>
/// Returns the correct file extension for the given string input
/// </summary>
/// <param name="input">
/// The string to parse.
/// </param>
/// <returns>
/// The correct file extension for the given string input if it can find one; otherwise an empty string.
/// </returns>
private string ParseExtension(string input)
{
Match match = FormatRegex.Match(input);
return match.Success ? match.Value : string.Empty;
}
#endregion
#endregion
}
}

346
src/ImageProcessor.Web/Caching/Copy of DiskCache.cs

@ -0,0 +1,346 @@
// -----------------------------------------------------------------------
// <copyright file="DiskCache.cs" company="James South">
// Copyright (c) James South.
// Dual licensed under the MIT or GPL Version 2 licenses.
// </copyright>
// -----------------------------------------------------------------------
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
/// <summary>
/// Encapsulates methods to handle disk caching of images.
/// </summary>
internal sealed class DiskCache
{
#region Fields
/// <summary>
/// The maximum number of days to cache files on the system for.
/// </summary>
internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays;
/// <summary>
/// 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.
/// </summary>
private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789";
/// <summary>
/// The maximum number of files allowed in the directory.
/// </summary>
/// <remarks>
/// 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.
/// <see cref="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
/// <see cref="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
/// <see cref="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
/// </remarks>
private const int MaxFilesCount = 10000;
/// <summary>
/// The regular expression to search strings for file extensions.
/// </summary>
private static readonly Regex FormatRegex = new Regex(
@"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled);
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// The absolute path to virtual cache path on the server.
/// </summary>
private static readonly string AbsoluteCachePath =
HostingEnvironment.MapPath(ImageProcessorConfig.Instance.VirtualCachePath);
#endregion
#region Methods
/// <summary>
/// The create cache paths.
/// </summary>
/// <returns>
/// The true if the cache directories are created successfully; otherwise, false.
/// </returns>
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;
}
/// <summary>
/// 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
/// </summary>
/// <param name="imagePath">The original image path.</param>
/// <param name="imageName">The original image name.</param>
/// <returns>The full cached path for the image.</returns>
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;
}
/// <summary>
/// Adds an image to the cache.
/// </summary>
/// <param name="cachedPath">
/// The cached path.
/// </param>
/// <param name="lastWriteTimeUtc">
/// The last write time.
/// </param>
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);
}
/// <summary>
/// Converts an absolute file path
/// </summary>
/// <param name="absolutePath">The absolute path to convert.</param>
/// <param name="request">The <see cref="T:System.Web.HttpRequest"/>from the current context.</param>
/// <returns>The virtual path to the file.</returns>
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.");
}
/// <summary>
/// Returns a value indicating whether the original file has been updated.
/// </summary>
/// <param name="imagePath">The original image path.</param>
/// <param name="cachedImagePath">The cached image path.</param>
/// <param name="isRemote">Whether the file is a remote request.</param>
/// <returns>
/// True if the the original file has been updated; otherwise, false.
/// </returns>
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;
}
/// <summary>
/// Sets the LastWriteTime of the cached file to match the original file.
/// </summary>
/// <param name="imagePath">
/// The original image path.
/// </param>
/// <param name="cachedImagePath">
/// The cached image path.
/// </param>
/// <param name="isRemote">Whether the file is remote.</param>
/// <returns>
/// The <see cref="System.DateTime"/> set to the last write time of the file.
/// </returns>
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();
}
/// <summary>
/// Purges any files from the file-system cache in the given folders.
/// </summary>
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<string, CachedImage> 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;
}
}
}
}
/// <summary>
/// Returns the correct file extension for the given string input
/// </summary>
/// <param name="input">
/// The string to parse.
/// </param>
/// <returns>
/// The correct file extension for the given string input if it can find one; otherwise an empty string.
/// </returns>
private static string ParseExtension(string input)
{
Match match = FormatRegex.Match(input);
return match.Success ? match.Value : string.Empty;
}
#endregion
}
}

174
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;
/// <summary>
/// Extensions related to the <see cref="Task"/> classes.
/// Supports implementing "async"-style methods in C#4 using iterators.
/// </summary>
/// <remarks>
/// I would call this TaskExtensions, except that clients must name the class to use methods like <see cref="FromResult{T}(T)"/>.
/// Based on work from Await Tasks in C#4 using Iterators by Keith L Robertson.
/// <see cref="http://www.codeproject.com/Articles/504197/Await-Tasks-in-Csharp4-using-Iterators"/>
/// </remarks>
public static class TaskEx
{
/// <summary>
/// Return a Completed <see cref="Task{TResult}"/> with a specific <see cref="Task{TResult}.Result"/> value.
/// </summary>
/// <typeparam name="TResult">
/// The result
/// </typeparam>
/// <param name="resultValue">
/// The result Value.
/// </param>
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public static Task<TResult> FromResult<TResult>(TResult resultValue)
{
var completionSource = new TaskCompletionSource<TResult>();
completionSource.SetResult(resultValue);
return completionSource.Task;
}
/// <summary>
/// Transform an enumeration of <see cref="Task"/> into a single non-Result <see cref="Task"/>.
/// </summary>
/// <param name="tasks">
/// The tasks.
/// </param>
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public static Task ToTask(this IEnumerable<Task> tasks)
{
return ToTask<VoidResult>(tasks);
}
/// <summary>
/// Transform an enumeration of <see cref="Task"/> into a single <see cref="Task{TResult}"/>.
/// The final <see cref="Task"/> in <paramref name="tasks"/> must be a <see cref="Task{TResult}"/>.
/// </summary>
/// <typeparam name="TResult">
/// The task results
/// </typeparam>
/// <param name="tasks">
/// The tasks.
/// </param>
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks)
{
var taskScheduler =
SynchronizationContext.Current == null
? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext();
var taskEnumerator = tasks.GetEnumerator();
var completionSource = new TaskCompletionSource<TResult>();
// Clean up the enumerator when the task completes.
completionSource.Task.ContinueWith(t => taskEnumerator.Dispose(), taskScheduler);
ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, null);
return completionSource.Task;
}
/// <summary>
/// If the previous task Canceled or Faulted, complete the master task with the same <see cref="Task.Status"/>.
/// Obtain the next <see cref="Task"/> from the <paramref name="taskEnumerator"/>.
/// If none, complete the master task, possibly with the <see cref="Task{T}.Result"/> of the last task.
/// Otherwise, set up the task with a continuation to come do this again when it completes.
/// </summary>
private static void ToTaskDoOneStep<TResult>(
IEnumerator<Task> taskEnumerator, TaskScheduler taskScheduler,
TaskCompletionSource<TResult> 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<TResult>))
{ // Wrong result
completionSource.SetException(new InvalidOperationException(
"Asynchronous iterator " + taskEnumerator +
" requires a final result task of type " + typeof(Task<TResult>).FullName +
(completedTask == null ? ", but none was provided." :
"; the actual task type was " + completedTask.GetType().FullName)));
}
else
{
completionSource.SetResult(((Task<TResult>)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);
}
}
/// <summary>
/// Internal marker type for using <see cref="ToTask{T}"/> to implement <see cref="ToTask"/>.
/// </summary>
private abstract class VoidResult
{
}
}
}

2
src/ImageProcessor.Web/ImageProcessor.Web.csproj

@ -87,7 +87,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Caching\CachedImage.cs" />
<Compile Include="Caching\Cacher.cs" />
<Compile Include="Caching\DiskCache.cs" />
<Compile Include="Helpers\TaskEx.cs" />
<Compile Include="Helpers\LockedDictionary.cs" />
<Compile Include="Caching\PersistantDictionary.cs" />
<Compile Include="Caching\SQLContext.cs" />

Loading…
Cancel
Save