Browse Source

Removing SQLite

Cache is now purely file based. Very fast!


Former-commit-id: 6880348d89c725fa1e7a20223fe10eb5fdb72f9d
af/merge-core
James South 12 years ago
parent
commit
8ee44a36f0
  1. 6
      src/.nuget/NuGet.Config
  2. 1
      src/.nuget/NuGet.exe.REMOVED.git-id
  3. 136
      src/.nuget/NuGet.targets
  4. 6
      src/ImageProcessor.Web/NET4/ImageProcessor.Web.csproj
  5. 123
      src/ImageProcessor.Web/NET45/Caching/CacheIndexer.cs
  6. 29
      src/ImageProcessor.Web/NET45/Caching/CachedImage.cs
  7. 57
      src/ImageProcessor.Web/NET45/Caching/DiskCache.cs
  8. 150
      src/ImageProcessor.Web/NET45/Caching/SQLContext.cs
  9. 11
      src/ImageProcessor.Web/NET45/Helpers/TaskHelpers.cs
  10. 4
      src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs
  11. 15
      src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj
  12. 1
      src/ImageProcessor.Web/NET45/SQLite.cs.REMOVED.git-id
  13. 486
      src/ImageProcessor.Web/NET45/SQLiteAsync.cs
  14. 5
      src/ImageProcessor.Web/NET45/packages.config
  15. 7
      src/ImageProcessor.sln
  16. 16
      src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj
  17. 4
      src/TestWebsites/NET45/Test_Website_NET45/packages.config
  18. 1
      src/packages/repositories.config

6
src/.nuget/NuGet.Config

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
</configuration>

1
src/.nuget/NuGet.exe.REMOVED.git-id

@ -0,0 +1 @@
8f613402956f9802681f150b1cb51f8400eb628e

136
src/.nuget/NuGet.targets

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
<!-- Enable the restore command to run before builds -->
<RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>
<!-- Property that enables building a package from a project -->
<BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>
<!-- Determines if package restore consent is required to restore packages -->
<RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>
<!-- Download NuGet.exe if it does not already exist -->
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageSources)' == '' ">
<!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
<!-- The official NuGet package source (https://www.nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
<!--
<PackageSource Include="https://www.nuget.org/api/v2/" />
<PackageSource Include="https://my-nuget-source/nuget/" />
-->
</ItemGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
<PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
<PackagesConfig>packages.config</PackagesConfig>
</PropertyGroup>
<PropertyGroup>
<!-- NuGet command -->
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\NuGet.exe</NuGetExePath>
<PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>
<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>
<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
<RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
<NonInteractiveSwitch Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' ">-NonInteractive</NonInteractiveSwitch>
<PaddedSolutionDir Condition=" '$(OS)' == 'Windows_NT'">"$(SolutionDir) "</PaddedSolutionDir>
<PaddedSolutionDir Condition=" '$(OS)' != 'Windows_NT' ">"$(SolutionDir)"</PaddedSolutionDir>
<!-- Commands -->
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)</RestoreCommand>
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols</BuildCommand>
<!-- We need to ensure packages are restored prior to assembly resolve -->
<BuildDependsOn Condition="$(RestorePackages) == 'true'">
RestorePackages;
$(BuildDependsOn);
</BuildDependsOn>
<!-- Make the build depend on restore packages -->
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
$(BuildDependsOn);
BuildPackage;
</BuildDependsOn>
</PropertyGroup>
<Target Name="CheckPrerequisites">
<!-- Raise an error if we're unable to locate nuget.exe -->
<Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
<!--
Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
This effectively acts as a lock that makes sure that the download operation will only happen once and all
parallel builds will have to wait for it to complete.
-->
<MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT;DownloadNuGetExe=$(DownloadNuGetExe)" />
</Target>
<Target Name="_DownloadNuGet">
<DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
</Target>
<Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(RestoreCommand)"
Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />
<Exec Command="$(RestoreCommand)"
LogStandardErrorAsError="true"
Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
</Target>
<Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(BuildCommand)"
Condition=" '$(OS)' != 'Windows_NT' " />
<Exec Command="$(BuildCommand)"
LogStandardErrorAsError="true"
Condition=" '$(OS)' == 'Windows_NT' " />
</Target>
<UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<OutputFilename ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Net" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
OutputFilename = Path.GetFullPath(OutputFilename);
Log.LogMessage("Downloading latest version of NuGet.exe...");
WebClient webClient = new WebClient();
webClient.DownloadFile("https://www.nuget.org/nuget.exe", OutputFilename);
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
</Project>

6
src/ImageProcessor.Web/NET4/ImageProcessor.Web.csproj

@ -12,6 +12,8 @@
<AssemblyName>ImageProcessor.Web</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -82,7 +84,6 @@
<Compile Include="..\NET45\Caching\MemCache.cs">
<Link>MemCache.cs</Link>
</Compile>
<Compile Include="..\NET45\Caching\SQLContext.cs" />
<Compile Include="..\NET45\Config\ImageCacheSection.cs" />
<Compile Include="..\NET45\Config\ImageProcessingSection.cs" />
<Compile Include="..\NET45\Config\ImageProcessorConfig.cs" />
@ -95,8 +96,6 @@
<Link>Preset.cs</Link>
</Compile>
<Compile Include="..\NET45\Properties\AssemblyInfo.cs" />
<Compile Include="..\NET45\SQLite.cs" />
<Compile Include="..\NET45\SQLiteAsync.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ImageProcessor\ImageProcessor.csproj">
@ -118,6 +117,7 @@
xcopy /y "$(TargetDir)$(TargetName).pdb" "$(SolutionDir)\TestWebsites\NET4\bin"</PostBuildEvent>
</PropertyGroup>
<Import Project="..\..\packages\Microsoft.Bcl.Build.1.0.8\tools\Microsoft.Bcl.Build.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

123
src/ImageProcessor.Web/NET45/Caching/CacheIndexer.cs

@ -11,73 +11,42 @@
namespace ImageProcessor.Web.Caching
{
#region Using
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Caching;
using System.Threading.Tasks;
using ImageProcessor.Web.Helpers;
#endregion
/// <summary>
/// Represents an in memory collection of keys and values whose operations are concurrent.
/// </summary>
internal sealed class CacheIndexer
internal static class CacheIndexer
{
#region Fields
/// <summary>
/// A new instance Initializes a new instance of the <see cref="T:ImageProcessor.Web.Caching.CacheIndexer"/> class.
/// initialized lazily.
/// </summary>
private static readonly Lazy<CacheIndexer> Lazy =
new Lazy<CacheIndexer>(() => new CacheIndexer());
/// <summary>
/// The object to lock against.
/// </summary>
private static readonly object SyncRoot = new object();
#endregion
#region Constructors
/// <summary>
/// Prevents a default instance of the <see cref="T:ImageProcessor.Web.Caching.CacheIndexer"/> class
/// from being created.
/// </summary>
private CacheIndexer()
{
this.LoadCache();
}
#endregion
/// <summary>
/// Gets the current instance of the <see cref="T:ImageProcessor.Web.Caching.CacheIndexer"/> class.
/// </summary>
public static CacheIndexer Instance
{
get
{
return Lazy.Value;
}
}
#region Public
/// <summary>
/// Gets the <see cref="CachedImage"/> associated with the specified key.
/// </summary>
/// <param name="key">
/// The key of the value to get.
/// <param name="cachedPath">
/// The cached path of the value to get.
/// </param>
/// <returns>
/// The <see cref="CachedImage"/> matching the given key if the <see cref="CacheIndexer"/> contains an element with
/// the specified key; otherwise, null.
/// </returns>
public async Task<CachedImage> GetValueAsync(string key)
public static async Task<CachedImage> GetValueAsync(string cachedPath)
{
string key = Path.GetFileNameWithoutExtension(cachedPath);
CachedImage cachedImage = (CachedImage)MemCache.GetItem(key);
if (cachedImage == null)
{
cachedImage = await SQLContext.GetImageAsync(key);
cachedImage = await TaskHelpers.Run(() => GetCachedImage(cachedPath));
if (cachedImage != null)
{
MemCache.AddItem(key, cachedImage);
Add(cachedImage);
}
}
@ -87,89 +56,63 @@ namespace ImageProcessor.Web.Caching
/// <summary>
/// Removes the value associated with the specified key.
/// </summary>
/// <param name="key">
/// <param name="cachedPath">
/// The key of the item to remove.
/// </param>
/// <returns>
/// true if the <see cref="CacheIndexer"/> removes an element with
/// the specified key; otherwise, false.
/// </returns>
public async Task<bool> RemoveAsync(string key)
public static bool Remove(string cachedPath)
{
if (await this.SaveCacheAsync(key, null, true) > 0)
{
MemCache.RemoveItem(key);
return true;
}
return false;
string key = Path.GetFileNameWithoutExtension(cachedPath);
return MemCache.RemoveItem(key);
}
/// <summary>
/// Adds the specified key and value to the dictionary or returns the value if it exists.
/// </summary>
/// <param name="key">
/// The key.
/// </param>
/// <param name="cachedImage">
/// The cached image to add.
/// </param>
/// <returns>
/// The value of the item to add or get.
/// </returns>
public async Task<CachedImage> AddAsync(string key, CachedImage cachedImage)
public static CachedImage Add(CachedImage cachedImage)
{
// Add the CachedImage.
if (await this.SaveCacheAsync(key, cachedImage, false) > 0)
{
MemCache.AddItem(key, cachedImage);
}
CacheItemPolicy policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string>() { cachedImage.Path }));
MemCache.AddItem(cachedImage.Key, cachedImage, policy);
return cachedImage;
}
#endregion
/// <summary>
/// Saves the image to the file-system cache.
/// Creates a new cached image from the cache instance on disk.
/// </summary>
/// <param name="key">
/// The key.
/// </param>
/// <param name="cachedImage">
/// The cached Image.
/// </param>
/// <param name="remove">
/// The remove.
/// <param name="cachePath">
/// The cache path.
/// </param>
/// <returns>
/// true, if the dictionary is saved to the file-system; otherwise, false.
/// The <see cref="CachedImage"/> from the cache instance on disk.
/// </returns>
private async Task<int> SaveCacheAsync(string key, CachedImage cachedImage, bool remove)
private static CachedImage GetCachedImage(string cachePath)
{
try
{
if (remove)
{
return await SQLContext.RemoveImageAsync(key);
}
FileInfo fileInfo = new FileInfo(cachePath);
return await SQLContext.AddImageAsync(cachedImage);
}
catch
if (!fileInfo.Exists)
{
return 0;
return null;
}
}
/// <summary>
/// Loads the cache file to populate the in memory cache.
/// </summary>
private void LoadCache()
{
lock (SyncRoot)
return new CachedImage
{
SQLContext.CreateDatabase();
}
Key = Path.GetFileNameWithoutExtension(cachePath),
Path = cachePath,
LastWriteTimeUtc = fileInfo.LastWriteTimeUtc
};
}
}
}

29
src/ImageProcessor.Web/NET45/Caching/CachedImage.cs

@ -1,17 +1,7 @@
// -----------------------------------------------------------------------
// <copyright file="CachedImage.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// -----------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
namespace ImageProcessor.Web.Caching
{
#region Using
using System;
using SQLite;
#endregion
/// <summary>
@ -19,16 +9,9 @@ namespace ImageProcessor.Web.Caching
/// </summary>
public sealed class CachedImage
{
/// <summary>
/// Gets or sets the id identifying the cached image.
/// </summary>
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
/// <summary>
/// Gets or sets the key identifying the cached image.
/// </summary>
[Unique]
public string Key { get; set; }
/// <summary>
@ -36,19 +19,9 @@ namespace ImageProcessor.Web.Caching
/// </summary>
public string Path { get; set; }
/// <summary>
/// Gets or sets the maximum age of the cached image in days.
/// </summary>
public int MaxAge { get; set; }
/// <summary>
/// Gets or sets the last write time of the cached image.
/// </summary>
public DateTime LastWriteTimeUtc { get; set; }
/// <summary>
/// Gets or sets when the cached image should expire from the cache.
/// </summary>
public DateTime ExpiresUtc { get; set; }
}
}

57
src/ImageProcessor.Web/NET45/Caching/DiskCache.cs

@ -145,23 +145,17 @@ namespace ImageProcessor.Web.Caching
/// <param name="lastWriteTimeUtc">
/// The last write time.
/// </param>
/// <returns>
/// The <see cref="T:System.Threading.Tasks.Task"/>.
/// </returns>
internal async Task AddImageToCacheAsync(DateTime lastWriteTimeUtc)
internal void AddImageToCache(DateTime lastWriteTimeUtc)
{
string key = Path.GetFileNameWithoutExtension(this.CachedPath);
DateTime expires = DateTime.UtcNow.AddDays(MaxFileCachedDuration).ToUniversalTime();
CachedImage cachedImage = new CachedImage
{
Key = key,
Path = this.CachedPath,
MaxAge = MaxFileCachedDuration,
LastWriteTimeUtc = lastWriteTimeUtc,
ExpiresUtc = expires
LastWriteTimeUtc = lastWriteTimeUtc
};
await CacheIndexer.Instance.AddAsync(key, cachedImage);
CacheIndexer.Add(cachedImage);
}
/// <summary>
@ -172,25 +166,22 @@ namespace ImageProcessor.Web.Caching
/// </returns>
internal async Task<bool> IsNewOrUpdatedFileAsync()
{
string key = Path.GetFileNameWithoutExtension(this.CachedPath);
CachedImage cachedImage;
string path = this.CachedPath;
bool isUpdated = false;
CachedImage cachedImage;
if (this.isRemote)
{
cachedImage = await CacheIndexer.Instance.GetValueAsync(key);
cachedImage = await CacheIndexer.GetValueAsync(path);
if (cachedImage != null)
{
// 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 (cachedImage.LastWriteTimeUtc.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration))
{
if (await CacheIndexer.Instance.RemoveAsync(key))
{
isUpdated = true;
}
CacheIndexer.Remove(path);
isUpdated = true;
}
}
else
@ -202,7 +193,7 @@ namespace ImageProcessor.Web.Caching
else
{
// Test now for locally requested files.
cachedImage = await CacheIndexer.Instance.GetValueAsync(key);
cachedImage = await CacheIndexer.GetValueAsync(path);
if (cachedImage != null)
{
@ -213,13 +204,10 @@ namespace ImageProcessor.Web.Caching
// 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 (!this.RoughDateTimeCompare(imageFileInfo.LastWriteTimeUtc, cachedImage.LastWriteTimeUtc)
|| cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)
|| cachedImage.MaxAge != MaxFileCachedDuration)
|| cachedImage.LastWriteTimeUtc.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration))
{
if (await CacheIndexer.Instance.RemoveAsync(key))
{
isUpdated = true;
}
CacheIndexer.Remove(path);
isUpdated = true;
}
}
}
@ -241,10 +229,9 @@ namespace ImageProcessor.Web.Caching
/// </returns>
internal async Task<DateTime> GetLastWriteTimeAsync()
{
string key = Path.GetFileNameWithoutExtension(this.CachedPath);
DateTime dateTime = DateTime.UtcNow;
CachedImage cachedImage = await CacheIndexer.Instance.GetValueAsync(key);
CachedImage cachedImage = await CacheIndexer.GetValueAsync(this.CachedPath);
if (cachedImage != null)
{
@ -322,7 +309,7 @@ namespace ImageProcessor.Web.Caching
/// <param name="path">
/// The path to the folder.
/// </param>
private async void TrimCachedFolder(string path)
private void TrimCachedFolder(string path)
{
// ReSharper disable once AssignNullToNotNullAttribute
DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(path));
@ -341,12 +328,9 @@ namespace ImageProcessor.Web.Caching
}
// Remove from the cache and delete each CachedImage.
string key = Path.GetFileNameWithoutExtension(fileInfo.Name);
if (await CacheIndexer.Instance.RemoveAsync(key))
{
fileInfo.Delete();
count -= 1;
}
CacheIndexer.Remove(fileInfo.Name);
fileInfo.Delete();
count -= 1;
}
// ReSharper disable once EmptyGeneralCatchClause
catch
@ -361,7 +345,6 @@ namespace ImageProcessor.Web.Caching
/// The images are stored in paths that are based upon the sha1 of their full request path
/// taking the individual characters of the hash to determine their location.
/// This allows us to store millions of images.
/// Answers on a post card if you can figure out a way to store that many details in a db for fast recovery.
/// </summary>
/// <returns>The full cached path for the image.</returns>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")]
@ -378,8 +361,8 @@ namespace ImageProcessor.Web.Caching
string fallbackExtension = this.imageName.Substring(this.imageName.LastIndexOf(".", StringComparison.Ordinal) + 1);
string encryptedName = this.fullPath.ToSHA1Fingerprint();
// Collision rate of about 1 in 1000 for the folder structure.
string pathFromKey = string.Join("\\", encryptedName.ToCharArray().Take(5));
// Collision rate of about 1 in 10000 for the folder structure.
string pathFromKey = string.Join("\\", encryptedName.ToCharArray().Take(6));
string cachedFileName = string.Format(
"{0}.{1}",

150
src/ImageProcessor.Web/NET45/Caching/SQLContext.cs

@ -1,150 +0,0 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SQLContext.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Provides a wrapper for the SQLite functionality.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
{
#region Using
using System.IO;
using System.Threading.Tasks;
using System.Web.Hosting;
using ImageProcessor.Web.Config;
using SQLite;
#endregion
/// <summary>
/// Provides a wrapper for the SQLite functionality.
/// </summary>
internal sealed class SQLContext
{
#region Fields
/// <summary>
/// The default path for cached folders on the server.
/// </summary>
private static readonly string VirtualCachePath = ImageProcessorConfig.Instance.VirtualCachePath;
/// <summary>
/// The cached index location.
/// </summary>
private static readonly string IndexLocation = Path.Combine(HostingEnvironment.MapPath(VirtualCachePath), "cache.db");
/// <summary>
/// The connection string.
/// </summary>
private static readonly string ConnectionString = IndexLocation;
#endregion
#region Methods
#region Internal
/// <summary>
/// Creates the database if it doesn't already exist.
/// </summary>
internal static void CreateDatabase()
{
try
{
if (!File.Exists(IndexLocation))
{
string absolutePath = HostingEnvironment.MapPath(VirtualCachePath);
if (absolutePath != null)
{
DirectoryInfo directoryInfo = new DirectoryInfo(absolutePath);
if (!directoryInfo.Exists)
{
// Create the directory.
Directory.CreateDirectory(absolutePath);
}
}
using (SQLiteConnection connection = new SQLiteConnection(IndexLocation))
{
connection.CreateTable<CachedImage>();
}
}
}
catch
{
throw;
}
}
/// <summary>
/// Gets a cached image from the database.
/// </summary>
/// <param name="key">
/// The key for the cached image to get.
/// </param>
/// <returns>
/// The <see cref="CachedImage"/> from the database.
/// </returns>
internal static async Task<CachedImage> GetImageAsync(string key)
{
try
{
SQLiteAsyncConnection connection = new SQLiteAsyncConnection(ConnectionString);
return await connection.GetAsync<CachedImage>(c => c.Key == key);
}
catch
{
return null;
}
}
/// <summary>
/// Adds a cached image to the database.
/// </summary>
/// <param name="image">
/// The cached image to add.
/// </param>
/// <returns>
/// The true if the addition of the cached image is added; otherwise, false.
/// </returns>
internal static async Task<int> AddImageAsync(CachedImage image)
{
try
{
SQLiteAsyncConnection connection = new SQLiteAsyncConnection(ConnectionString);
return await connection.InsertAsync(image);
}
catch
{
return 0;
}
}
/// <summary>
/// Removes a cached image from the database.
/// </summary>
/// <param name="key">
/// The key for the cached image.
/// </param>
/// <returns>
/// The true if the addition of the cached image is removed; otherwise, false.
/// </returns>
internal static async Task<int> RemoveImageAsync(string key)
{
try
{
SQLiteAsyncConnection connection = new SQLiteAsyncConnection(ConnectionString);
CachedImage cachedImage = await connection.GetAsync<CachedImage>(c => c.Key == key);
return await connection.DeleteAsync(cachedImage);
}
catch
{
return 0;
}
}
#endregion
#endregion
}
}

11
src/ImageProcessor.Web/NET45/Helpers/TaskHelpers.cs

@ -1,9 +1,12 @@
// -----------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="TaskHelpers.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// -----------------------------------------------------------------------
// <summary>
// Provides some syntactic sugar to run tasks.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Helpers
{

4
src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs

@ -303,7 +303,7 @@ namespace ImageProcessor.Web.HttpModules
DateTime dateTime = await cache.SetCachedLastWriteTimeAsync();
// Add to the cache.
await cache.AddImageToCacheAsync(dateTime);
cache.AddImageToCache(dateTime);
// Trim the cache.
await cache.TrimCachedFolderAsync(cachedPath);
@ -320,7 +320,7 @@ namespace ImageProcessor.Web.HttpModules
DateTime dateTime = await cache.SetCachedLastWriteTimeAsync();
// Add to the cache.
await cache.AddImageToCacheAsync(dateTime);
cache.AddImageToCache(dateTime);
// Trim the cache.
await cache.TrimCachedFolderAsync(cachedPath);

15
src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj

@ -11,6 +11,8 @@
<AssemblyName>ImageProcessor.Web</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -30,12 +32,6 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Community.CsharpSqlite">
<HintPath>..\..\packages\Csharp-Sqlite.3.7.7.1\lib\net40\Community.CsharpSqlite.dll</HintPath>
</Reference>
<Reference Include="Community.CsharpSqlite.SQLiteClient">
<HintPath>..\..\packages\Csharp-Sqlite.3.7.7.1\lib\net40\Community.CsharpSqlite.SQLiteClient.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
@ -53,7 +49,6 @@
<Compile Include="Caching\MemCache.cs" />
<Compile Include="Caching\DiskCache.cs" />
<Compile Include="Caching\CacheIndexer.cs" />
<Compile Include="Caching\SQLContext.cs" />
<Compile Include="Config\ImageCacheSection.cs" />
<Compile Include="Config\ImageProcessingSection.cs" />
<Compile Include="Config\ImageProcessorConfig.cs" />
@ -64,8 +59,6 @@
<Compile Include="ImageFactoryExtensions.cs" />
<Compile Include="Preset.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SQLite.cs" />
<Compile Include="SQLiteAsync.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ImageProcessor\ImageProcessor.csproj">
@ -73,10 +66,8 @@
<Name>ImageProcessor</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

1
src/ImageProcessor.Web/NET45/SQLite.cs.REMOVED.git-id

@ -1 +0,0 @@
ce0491dcbe39702bf25fb616f76e1b149f670688

486
src/ImageProcessor.Web/NET45/SQLiteAsync.cs

@ -1,486 +0,0 @@
//
// Copyright (c) 2012 Krueger Systems, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace SQLite
{
public partial class SQLiteAsyncConnection
{
SQLiteConnectionString _connectionString;
public SQLiteAsyncConnection (string databasePath, bool storeDateTimeAsTicks = false)
{
_connectionString = new SQLiteConnectionString (databasePath, storeDateTimeAsTicks);
}
SQLiteConnectionWithLock GetConnection ()
{
return SQLiteConnectionPool.Shared.GetConnection (_connectionString);
}
public Task<CreateTablesResult> CreateTableAsync<T> ()
where T : new ()
{
return CreateTablesAsync (typeof (T));
}
public Task<CreateTablesResult> CreateTablesAsync<T, T2> ()
where T : new ()
where T2 : new ()
{
return CreateTablesAsync (typeof (T), typeof (T2));
}
public Task<CreateTablesResult> CreateTablesAsync<T, T2, T3> ()
where T : new ()
where T2 : new ()
where T3 : new ()
{
return CreateTablesAsync (typeof (T), typeof (T2), typeof (T3));
}
public Task<CreateTablesResult> CreateTablesAsync<T, T2, T3, T4> ()
where T : new ()
where T2 : new ()
where T3 : new ()
where T4 : new ()
{
return CreateTablesAsync (typeof (T), typeof (T2), typeof (T3), typeof (T4));
}
public Task<CreateTablesResult> CreateTablesAsync<T, T2, T3, T4, T5> ()
where T : new ()
where T2 : new ()
where T3 : new ()
where T4 : new ()
where T5 : new ()
{
return CreateTablesAsync (typeof (T), typeof (T2), typeof (T3), typeof (T4), typeof (T5));
}
public Task<CreateTablesResult> CreateTablesAsync (params Type[] types)
{
return Task.Factory.StartNew (() => {
CreateTablesResult result = new CreateTablesResult ();
var conn = GetConnection ();
using (conn.Lock ()) {
foreach (Type type in types) {
int aResult = conn.CreateTable (type);
result.Results[type] = aResult;
}
}
return result;
});
}
public Task<int> DropTableAsync<T> ()
where T : new ()
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.DropTable<T> ();
}
});
}
public Task<int> InsertAsync (object item)
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Insert (item);
}
});
}
public Task<int> UpdateAsync (object item)
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Update (item);
}
});
}
public Task<int> DeleteAsync (object item)
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Delete (item);
}
});
}
public Task<T> GetAsync<T>(object pk)
where T : new()
{
return Task.Factory.StartNew(() =>
{
var conn = GetConnection();
using (conn.Lock())
{
return conn.Get<T>(pk);
}
});
}
public Task<T> FindAsync<T> (object pk)
where T : new ()
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Find<T> (pk);
}
});
}
public Task<T> GetAsync<T> (Expression<Func<T, bool>> predicate)
where T : new()
{
return Task.Factory.StartNew(() =>
{
var conn = GetConnection();
using (conn.Lock())
{
return conn.Get<T> (predicate);
}
});
}
public Task<T> FindAsync<T> (Expression<Func<T, bool>> predicate)
where T : new ()
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Find<T> (predicate);
}
});
}
public Task<int> ExecuteAsync (string query, params object[] args)
{
return Task<int>.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Execute (query, args);
}
});
}
public Task<int> InsertAllAsync (IEnumerable items)
{
return Task.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.InsertAll (items);
}
});
}
[Obsolete("Will cause a deadlock if any call in action ends up in a different thread. Use RunInTransactionAsync(Action<SQLiteConnection>) instead.")]
public Task RunInTransactionAsync (Action<SQLiteAsyncConnection> action)
{
return Task.Factory.StartNew (() => {
var conn = this.GetConnection ();
using (conn.Lock ()) {
conn.BeginTransaction ();
try {
action (this);
conn.Commit ();
}
catch (Exception) {
conn.Rollback ();
throw;
}
}
});
}
public Task RunInTransactionAsync(Action<SQLiteConnection> action)
{
return Task.Factory.StartNew(() =>
{
var conn = this.GetConnection();
using (conn.Lock())
{
conn.BeginTransaction();
try
{
action(conn);
conn.Commit();
}
catch (Exception)
{
conn.Rollback();
throw;
}
}
});
}
public AsyncTableQuery<T> Table<T> ()
where T : new ()
{
//
// This isn't async as the underlying connection doesn't go out to the database
// until the query is performed. The Async methods are on the query iteself.
//
var conn = GetConnection ();
return new AsyncTableQuery<T> (conn.Table<T> ());
}
public Task<T> ExecuteScalarAsync<T> (string sql, params object[] args)
{
return Task<T>.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
var command = conn.CreateCommand (sql, args);
return command.ExecuteScalar<T> ();
}
});
}
public Task<List<T>> QueryAsync<T> (string sql, params object[] args)
where T : new ()
{
return Task<List<T>>.Factory.StartNew (() => {
var conn = GetConnection ();
using (conn.Lock ()) {
return conn.Query<T> (sql, args);
}
});
}
}
//
// TODO: Bind to AsyncConnection.GetConnection instead so that delayed
// execution can still work after a Pool.Reset.
//
public class AsyncTableQuery<T>
where T : new ()
{
TableQuery<T> _innerQuery;
public AsyncTableQuery (TableQuery<T> innerQuery)
{
_innerQuery = innerQuery;
}
public AsyncTableQuery<T> Where (Expression<Func<T, bool>> predExpr)
{
return new AsyncTableQuery<T> (_innerQuery.Where (predExpr));
}
public AsyncTableQuery<T> Skip (int n)
{
return new AsyncTableQuery<T> (_innerQuery.Skip (n));
}
public AsyncTableQuery<T> Take (int n)
{
return new AsyncTableQuery<T> (_innerQuery.Take (n));
}
public AsyncTableQuery<T> OrderBy<U> (Expression<Func<T, U>> orderExpr)
{
return new AsyncTableQuery<T> (_innerQuery.OrderBy<U> (orderExpr));
}
public AsyncTableQuery<T> OrderByDescending<U> (Expression<Func<T, U>> orderExpr)
{
return new AsyncTableQuery<T> (_innerQuery.OrderByDescending<U> (orderExpr));
}
public Task<List<T>> ToListAsync ()
{
return Task.Factory.StartNew (() => {
using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) {
return _innerQuery.ToList ();
}
});
}
public Task<int> CountAsync ()
{
return Task.Factory.StartNew (() => {
using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) {
return _innerQuery.Count ();
}
});
}
public Task<T> ElementAtAsync (int index)
{
return Task.Factory.StartNew (() => {
using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) {
return _innerQuery.ElementAt (index);
}
});
}
public Task<T> FirstAsync ()
{
return Task<T>.Factory.StartNew(() => {
using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) {
return _innerQuery.First ();
}
});
}
public Task<T> FirstOrDefaultAsync ()
{
return Task<T>.Factory.StartNew(() => {
using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) {
return _innerQuery.FirstOrDefault ();
}
});
}
}
public class CreateTablesResult
{
public Dictionary<Type, int> Results { get; private set; }
internal CreateTablesResult ()
{
this.Results = new Dictionary<Type, int> ();
}
}
class SQLiteConnectionPool
{
class Entry
{
public SQLiteConnectionString ConnectionString { get; private set; }
public SQLiteConnectionWithLock Connection { get; private set; }
public Entry (SQLiteConnectionString connectionString)
{
ConnectionString = connectionString;
Connection = new SQLiteConnectionWithLock (connectionString);
}
public void OnApplicationSuspended ()
{
Connection.Dispose ();
Connection = null;
}
}
readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry> ();
readonly object _entriesLock = new object ();
static readonly SQLiteConnectionPool _shared = new SQLiteConnectionPool ();
/// <summary>
/// Gets the singleton instance of the connection tool.
/// </summary>
public static SQLiteConnectionPool Shared
{
get
{
return _shared;
}
}
public SQLiteConnectionWithLock GetConnection (SQLiteConnectionString connectionString)
{
lock (_entriesLock) {
Entry entry;
string key = connectionString.ConnectionString;
if (!_entries.TryGetValue (key, out entry)) {
entry = new Entry (connectionString);
_entries[key] = entry;
}
return entry.Connection;
}
}
/// <summary>
/// Closes all connections managed by this pool.
/// </summary>
public void Reset ()
{
lock (_entriesLock) {
foreach (var entry in _entries.Values) {
entry.OnApplicationSuspended ();
}
_entries.Clear ();
}
}
/// <summary>
/// Call this method when the application is suspended.
/// </summary>
/// <remarks>Behaviour here is to close any open connections.</remarks>
public void ApplicationSuspended ()
{
Reset ();
}
}
class SQLiteConnectionWithLock : SQLiteConnection
{
readonly object _lockPoint = new object ();
public SQLiteConnectionWithLock (SQLiteConnectionString connectionString)
: base (connectionString.DatabasePath, connectionString.StoreDateTimeAsTicks)
{
}
public IDisposable Lock ()
{
return new LockWrapper (_lockPoint);
}
private class LockWrapper : IDisposable
{
object _lockPoint;
public LockWrapper (object lockPoint)
{
_lockPoint = lockPoint;
Monitor.Enter (_lockPoint);
}
public void Dispose ()
{
Monitor.Exit (_lockPoint);
}
}
}
}

5
src/ImageProcessor.Web/NET45/packages.config

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages >
<package id="Csharp-Sqlite" version="3.7.7.1" targetFramework="net40" />
<package id="sqlite-net" version="1.0.7" targetFramework="net40" />
</packages>

7
src/ImageProcessor.sln

@ -21,6 +21,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Web", "Image
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Web_NET45", "ImageProcessor.Web\NET45\ImageProcessor.Web_NET45.csproj", "{D011A778-59C8-4BFA-A770-C350216BF161}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1E656CDE-124D-4FAF-837C-0EF1E192D418}"
ProjectSection(SolutionItems) = preProject
.nuget\NuGet.Config = .nuget\NuGet.Config
.nuget\NuGet.exe = .nuget\NuGet.exe
.nuget\NuGet.targets = .nuget\NuGet.targets
EndProjectSection
EndProject
Global
GlobalSection(TestCaseManagementSettings) = postSolution
CategoryFile = ImageProcessor.vsmdi

16
src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj

@ -20,6 +20,8 @@
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -40,6 +42,12 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.Web.Mvc.FixedDisplayModes">
<HintPath>..\..\..\packages\Microsoft.AspNet.Mvc.FixedDisplayModes.1.0.1\lib\net40\Microsoft.Web.Mvc.FixedDisplayModes.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>..\..\..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
@ -62,13 +70,6 @@
<Private>True</Private>
<HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Web.Mvc.FixedDisplayModes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<Private>True</Private>
<HintPath>..\packages\Microsoft.AspNet.Mvc.FixedDisplayModes.1.0.0\lib\net40\Microsoft.Web.Mvc.FixedDisplayModes.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http">
</Reference>
<Reference Include="System.Net.Http.Formatting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
@ -246,6 +247,7 @@
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

4
src/TestWebsites/NET45/Test_Website_NET45/packages.config

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.AspNet.Mvc" version="4.0.20710.0" targetFramework="net45" />
<package id="Microsoft.AspNet.Mvc.FixedDisplayModes" version="1.0.0" targetFramework="net45" />
<package id="Microsoft.AspNet.Mvc.FixedDisplayModes" version="1.0.1" targetFramework="net45" />
<package id="Microsoft.AspNet.Razor" version="2.0.20715.0" targetFramework="net45" />
<package id="Microsoft.AspNet.WebApi" version="4.0.20710.0" targetFramework="net45" />
<package id="Microsoft.AspNet.WebApi.Client" version="4.0.20710.0" targetFramework="net45" />
@ -10,5 +10,5 @@
<package id="Microsoft.AspNet.WebPages" version="2.0.20710.0" targetFramework="net45" />
<package id="Microsoft.Net.Http" version="2.0.20710.0" targetFramework="net45" />
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net45" />
<package id="Newtonsoft.Json" version="4.5.11" targetFramework="net45" />
<package id="Newtonsoft.Json" version="5.0.8" targetFramework="net45" />
</packages>

1
src/packages/repositories.config

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<repositories>
<repository path="..\ImageProcessor.Web\NET4\packages.config" />
<repository path="..\ImageProcessor.Web\NET45\packages.config" />
<repository path="..\TestWebsites\NET45\Test_Website_NET45\packages.config" />
</repositories>
Loading…
Cancel
Save