diff --git a/src/FileCache.UnitTests/FileCacheTest.cs b/src/FileCache.UnitTests/FileCacheTest.cs
index 8e8d919..b25918d 100644
--- a/src/FileCache.UnitTests/FileCacheTest.cs
+++ b/src/FileCache.UnitTests/FileCacheTest.cs
@@ -135,6 +135,8 @@ public void CacheSizeTest()
cacheSize.Should().NotBe(0);
_cache.Remove("foo");
+ cacheSize = _cache.GetCacheSize();
+ cacheSize.Should().Be(0);
cacheSize = _cache.CurrentCacheSize;
cacheSize.Should().Be(0);
}
@@ -385,5 +387,37 @@ public void CleanCacheTest()
_cache["foo"].Should().BeNull();
_cache["bar"].Should().NotBeNull();
}
+
+ [TestMethod]
+ public void RawFileCacheTest()
+ {
+ // Test with filename based payload
+ FileCache perfCache = new FileCache("filePayload");
+ perfCache.PayloadReadMode = FileCache.PayloadMode.Filename;
+ perfCache.PayloadWriteMode = FileCache.PayloadMode.RawBytes;
+
+ perfCache["mybytes"] = new byte[] { 4, 2 };
+ perfCache.Get("mybytes").Should().BeOfType(typeof(string));
+ File.Exists((string)perfCache.Get("mybytes")).Should().BeTrue();
+ }
+
+
+ [TestMethod]
+ public void RawFileWithExpiryTest()
+ {
+ // Test with sliding expiry
+ FileCache perfCacheWithExpiry = new FileCache("filePayloadExpiry");
+ var pol = new CacheItemPolicy();
+ pol.SlidingExpiration = new TimeSpan(0, 0, 2);
+ perfCacheWithExpiry.DefaultPolicy = pol;
+ perfCacheWithExpiry.FilenameAsPayloadSafetyMargin = new TimeSpan(0, 0, 1);
+ perfCacheWithExpiry.PayloadReadMode = FileCache.PayloadMode.Filename;
+ perfCacheWithExpiry.PayloadWriteMode = FileCache.PayloadMode.RawBytes;
+
+ perfCacheWithExpiry["mybytes"] = new byte[] { 4, 2 };
+ File.Exists((string)perfCacheWithExpiry.Get("mybytes")).Should().BeTrue();
+ Thread.Sleep(2000);
+ perfCacheWithExpiry.Get("mybytes").Should().BeNull();
+ }
}
}
diff --git a/src/FileCache.sln b/src/FileCache.sln
index 4d98691..6dec382 100644
--- a/src/FileCache.sln
+++ b/src/FileCache.sln
@@ -49,6 +49,18 @@ Global
{F7C96C00-C7CD-4503-B6BE-B79700D24F68}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F7C96C00-C7CD-4503-B6BE-B79700D24F68}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F7C96C00-C7CD-4503-B6BE-B79700D24F68}.Release|x86.ActiveCfg = Release|Any CPU
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Any CPU.Build.0 = Debug|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Mixed Platforms.Build.0 = Debug|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|x86.ActiveCfg = Debug|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|x86.Build.0 = Debug|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Any CPU.ActiveCfg = Release|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Any CPU.Build.0 = Release|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Mixed Platforms.ActiveCfg = Release|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Mixed Platforms.Build.0 = Release|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|x86.ActiveCfg = Release|x86
+ {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/FileCache/FileCache.cs b/src/FileCache/FileCache.cs
index 4f4c91d..2cabc27 100644
--- a/src/FileCache/FileCache.cs
+++ b/src/FileCache/FileCache.cs
@@ -30,6 +30,7 @@ public class FileCache : ObjectCache
// this is a file used to prevent multiple processes from trying to "clean" at the same time
private const string SemaphoreFile = "cache.sem";
private long _currentCacheSize = 0;
+ private PayloadMode _readMode = PayloadMode.Serializable;
public string CacheDir { get; protected set; }
@@ -43,6 +44,51 @@ public class FileCache : ObjectCache
///
public CacheItemPolicy DefaultPolicy { get; set; }
+ ///
+ /// Specified how the cache payload is to be handled.
+ ///
+ public enum PayloadMode
+ {
+ ///
+ /// Treat the payload a a serializable object.
+ ///
+ Serializable,
+ ///
+ /// Treat the payload as a file name. File content will be copied on add, while get returns the file name.
+ ///
+ Filename,
+ ///
+ /// Treat the paylad as raw bytes. A byte[] and readable streams are supported on add.
+ ///
+ RawBytes
+ }
+
+ ///
+ /// Specified whether the payload is deserialized or just the file name.
+ ///
+ public PayloadMode PayloadReadMode {
+ get => _readMode;
+ set {
+ if (value == PayloadMode.RawBytes)
+ {
+ throw new ArgumentException("The read mode cannot be set to RawBytes. Use the file name please.");
+ }
+ _readMode = value;
+ }
+ }
+
+ ///
+ /// Specified how the payload is to be handled on add operations.
+ ///
+ public PayloadMode PayloadWriteMode { get; set; } = PayloadMode.Serializable;
+
+ ///
+ /// The amount of time before expiry that a filename will be used as a payoad. I.e.
+ /// the amount of time the cache's user can safely use the file delivered as a payload.
+ /// Default 10 minutes.
+ ///
+ public TimeSpan FilenameAsPayloadSafetyMargin = TimeSpan.FromMinutes(10);
+
///
/// Used to determine how long the FileCache will wait for a file to become
/// available. Default (00:00:00) is indefinite. Should the timeout be
@@ -451,13 +497,13 @@ private long CacheSizeHelper(DirectoryInfo root)
long size = 0;
// Add file sizes.
- FileInfo[] fis = root.GetFiles();
+ var fis = root.EnumerateFiles();
foreach (FileInfo fi in fis)
{
size += fi.Length;
}
// Add subdirectory sizes.
- DirectoryInfo[] dis = root.GetDirectories();
+ var dis = root.EnumerateDirectories();
foreach (DirectoryInfo di in dis)
{
size += CacheSizeHelper(di);
@@ -509,8 +555,7 @@ public void Flush(DateTime minDate, string regionName = null)
private void FlushHelper(DirectoryInfo root, DateTime minDate)
{
// check files.
- FileInfo[] fis = root.GetFiles();
- foreach (FileInfo fi in fis)
+ foreach (FileInfo fi in root.EnumerateFiles())
{
//is the file stale?
if(minDate > File.GetLastAccessTime(fi.FullName))
@@ -520,8 +565,7 @@ private void FlushHelper(DirectoryInfo root, DateTime minDate)
}
// check subdirectories
- DirectoryInfo[] dis = root.GetDirectories();
- foreach (DirectoryInfo di in dis)
+ foreach (DirectoryInfo di in root.EnumerateDirectories())
{
FlushHelper(di, minDate);
}
@@ -536,7 +580,7 @@ private void FlushHelper(DirectoryInfo root, DateTime minDate)
public CacheItemPolicy GetPolicy(string key, string regionName = null)
{
CacheItemPolicy policy = new CacheItemPolicy();
- FileCachePayload payload = ReadFile(key, regionName) as FileCachePayload;
+ FileCachePayload payload = ReadFile(PayloadMode.Filename, key, regionName) as FileCachePayload;
if (payload != null)
{
try
@@ -556,7 +600,7 @@ public CacheItemPolicy GetPolicy(string key, string regionName = null)
///
///
///
- public string[] GetKeys(string regionName = null)
+ public IEnumerable GetKeys(string regionName = null)
{
string region = "";
if (string.IsNullOrEmpty(regionName) == false)
@@ -564,15 +608,13 @@ public string[] GetKeys(string regionName = null)
region = regionName;
}
string directory = Path.Combine(CacheDir, _cacheSubFolder, region);
- List keys = new List();
if (Directory.Exists(directory))
{
- foreach (string file in Directory.GetFiles(directory))
+ foreach (string file in Directory.EnumerateFiles(directory))
{
- keys.Add(Path.GetFileNameWithoutExtension(file));
+ yield return Path.GetFileNameWithoutExtension(file);
}
}
- return keys.ToArray();
}
#endregion
@@ -619,10 +661,11 @@ private FileStream GetStream(string path, FileMode mode, FileAccess access, File
///
/// This function serves to centralize file reads within this class.
///
- ///
+ /// the payload reading mode
+ ///
///
///
- private FileCachePayload ReadFile(string key, string regionName = null, SerializationBinder objectBinder = null)
+ private FileCachePayload ReadFile(PayloadMode mode, string key, string regionName = null, SerializationBinder objectBinder = null)
{
object data = null;
SerializableCacheItemPolicy policy = new SerializableCacheItemPolicy();
@@ -632,33 +675,18 @@ private FileCachePayload ReadFile(string key, string regionName = null, Serializ
if (File.Exists(cachePath))
{
- using (FileStream stream = GetStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ switch (mode)
{
- BinaryFormatter formatter = new BinaryFormatter();
-
- //AC: From http://spazzarama.com//2009/06/25/binary-deserialize-unable-to-find-assembly/
- // Needed to deserialize custom objects
- if (objectBinder != null)
- {
- //take supplied binder over default binder
- formatter.Binder = objectBinder;
- }
- else if (_binder != null)
- {
- formatter.Binder = _binder;
- }
- try
- {
- data = formatter.Deserialize(stream);
- }
- catch (SerializationException)
- {
- data = null;
- }
- finally
- {
- stream.Close();
- }
+ default:
+ case PayloadMode.Filename:
+ data = cachePath;
+ break;
+ case PayloadMode.Serializable:
+ data = DeserializePayloadData(objectBinder, cachePath);
+ break;
+ case PayloadMode.RawBytes:
+ data = LoadRawPayloadData(cachePath);
+ break;
}
}
if (File.Exists(policyPath))
@@ -675,10 +703,6 @@ private FileCachePayload ReadFile(string key, string regionName = null, Serializ
{
policy = new SerializableCacheItemPolicy();
}
- finally
- {
- stream.Close();
- }
}
}
payload.Payload = data;
@@ -686,36 +710,102 @@ private FileCachePayload ReadFile(string key, string regionName = null, Serializ
return payload;
}
+ private object LoadRawPayloadData(string cachePath)
+ {
+ throw new NotSupportedException("Reading raw payload is not currently supported.");
+ }
+
+ private object DeserializePayloadData(SerializationBinder objectBinder, string cachePath)
+ {
+ object data;
+ using (FileStream stream = GetStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+
+ //AC: From http://spazzarama.com//2009/06/25/binary-deserialize-unable-to-find-assembly/
+ // Needed to deserialize custom objects
+ if (objectBinder != null)
+ {
+ //take supplied binder over default binder
+ formatter.Binder = objectBinder;
+ }
+ else if (_binder != null)
+ {
+ formatter.Binder = _binder;
+ }
+
+ try
+ {
+ data = formatter.Deserialize(stream);
+ }
+ catch (SerializationException)
+ {
+ data = null;
+ }
+ }
+
+ return data;
+ }
+
///
/// This function serves to centralize file writes within this class
///
- private void WriteFile(string key, FileCachePayload data, string regionName = null)
+ private void WriteFile(PayloadMode mode, string key, FileCachePayload data, string regionName = null, bool policyUpdateOnly = false)
{
string cachedPolicy = GetPolicyPath(key, regionName);
string cachedItemPath = GetCachePath(key, regionName);
- //remove current item / policy from cache size calculations
- if(File.Exists(cachedItemPath))
- {
- CurrentCacheSize -= new FileInfo(cachedItemPath).Length;
- }
- if(File.Exists(cachedPolicy))
+
+ if (!policyUpdateOnly)
{
- CurrentCacheSize -= new FileInfo(cachedPolicy).Length;
- }
+ long oldBlobSize = 0;
+ if (File.Exists(cachedItemPath))
+ {
+ oldBlobSize = new FileInfo(cachedItemPath).Length;
+ }
- //write the object payload (lock the file so we can write to it and force others to wait for us to finish)
- using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
- {
- BinaryFormatter formatter = new BinaryFormatter();
- formatter.Serialize(stream, data.Payload);
+ switch (mode) {
+ case PayloadMode.Serializable:
+ using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+
+ BinaryFormatter formatter = new BinaryFormatter();
+ formatter.Serialize(stream, data.Payload);
+ }
+ break;
+ case PayloadMode.RawBytes:
+ using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+
+ if (data.Payload is byte[])
+ {
+ byte[] dataPayload = (byte[]) data.Payload;
+ stream.Write(dataPayload, 0, dataPayload.Length);
+ }
+ else if (data.Payload is Stream)
+ {
+ Stream dataPayload = (Stream) data.Payload;
+ dataPayload.CopyTo(stream);
+ // no close or the like, we are not the owner
+ }
+ }
+ break;
+
+ case PayloadMode.Filename:
+ File.Copy((string) data.Payload, cachedItemPath, true);
+ break;
+ }
//adjust cache size (while we have the file to ourselves)
- CurrentCacheSize += new FileInfo(cachedItemPath).Length;
+ CurrentCacheSize += new FileInfo(cachedItemPath).Length - oldBlobSize;
+ }
- stream.Close();
+ //remove current policy file from cache size calculations
+ if (File.Exists(cachedPolicy))
+ {
+ CurrentCacheSize -= new FileInfo(cachedPolicy).Length;
}
-
+
//write the cache policy
using (FileStream stream = GetStream(cachedPolicy, FileMode.Create, FileAccess.Write, FileShare.None))
{
@@ -868,7 +958,7 @@ public override object AddOrGetExisting(string key, object value, CacheItemPolic
}
SerializableCacheItemPolicy cachePolicy = new SerializableCacheItemPolicy(policy);
FileCachePayload newPayload = new FileCachePayload(value, cachePolicy);
- WriteFile(key, newPayload, regionName);
+ WriteFile(PayloadWriteMode, key, newPayload, regionName);
//As documented in the spec (http://msdn.microsoft.com/en-us/library/dd780602.aspx), return the old
//cached value or null
@@ -924,14 +1014,20 @@ public override DefaultCacheCapabilities DefaultCacheCapabilities
public override object Get(string key, string regionName = null)
{
- FileCachePayload payload = ReadFile(key, regionName) as FileCachePayload;
+ FileCachePayload payload = ReadFile(PayloadReadMode, key, regionName) as FileCachePayload;
string cachedItemPath = GetCachePath(key, regionName);
+ DateTime cutoff = DateTime.Now;
+ if (PayloadReadMode == PayloadMode.Filename)
+ {
+ cutoff += FilenameAsPayloadSafetyMargin;
+ }
+
//null payload?
if (payload != null)
{
//did the item expire?
- if (payload.Policy.AbsoluteExpiration < DateTime.Now)
+ if (payload.Policy.AbsoluteExpiration < cutoff)
{
//set the payload to null
payload.Payload = null;
@@ -953,7 +1049,7 @@ public override object Get(string key, string regionName = null)
if (payload.Policy.SlidingExpiration > new TimeSpan())
{
payload.Policy.AbsoluteExpiration = DateTime.Now.Add(payload.Policy.SlidingExpiration);
- WriteFile(cachedItemPath, payload, regionName);
+ WriteFile(PayloadWriteMode, cachedItemPath, payload, regionName, true);
}
}
@@ -1053,7 +1149,7 @@ public override object Remove(string key, string regionName = null)
// CT note: calling Get from remove leads to an infinite loop and stack overflow,
// so I replaced it with a simple ReadFile call. None of the code here actually
// uses this object returned, but just in case someone else's outside code does.
- FileCachePayload fcp = ReadFile(key, regionName);
+ FileCachePayload fcp = ReadFile(PayloadMode.Filename, key, regionName);
valueToDelete = fcp.Payload;
string path = GetCachePath(key, regionName);
CurrentCacheSize -= new FileInfo(path).Length;