Building a Custom Cache Provider
创建一个自定义缓存提供
下面的例子展示了将每个缓存页面存储在独立的文件中,尽管基于磁盘的缓存比基于内存的慢几个数量级,但使用他也有2个优势。
持久化缓存:因为缓存输出被存储在磁盘上,即使web程序被重启,还是能够存在的。如果这些数据的产生很费开销,这样做还是值得的。
低内存的使用率:当缓存也被使用,他从磁盘直接获得服务。他不需要把数据读回内存。这对于大的缓存页很有用,尤其是对于使用多个参数的查询页输出来说。
尽管这些解决方案工作的很好,但是对于专业的系统来说,不是很精致。
下面的例子演示了如何使用自定义的缓存提供。
例子中主要继承了OutputCacheProvider类,然后把缓存写到磁盘中
public override void Set(string key, object entry, DateTime utcExpiry)
{
CacheItem item = new CacheItem(entry, utcExpiry);
string path = ConvertKeyToPath(key);
// Overwrite it, even if it already exists.
using (FileStream file = File.OpenWrite(path))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(file, item);
}
}
具体内容如下:(不翻译了)
Table 11-1. Overridable Methods in the OutputCacheProvider
Method Description
Initialize() - Gives you a place to perform initialization tasks when the provider is first loaded,such as reading other settings from the web.config file. This is the only method inthis table that you don’t need to override.
Add() - Adds the item to the cache, if it doesn’t already exist. If the item does exist, this method should take no action.
Set() - Adds the item to the cache. If the item already exists, this method should overwrite it.
Get() - Retrieves an item from the cache, if it exists. This method must also enforce timebased expiration, by checking the expiration date and removing the item if necessary.
Remove() Removes the item from the cache.
In this example, the custom cache provider is called FileCacheProvider:
public class FileCacheProvider : OutputCacheProvider
{
// The location where cached files will be placed.
public string CachePath
{ get; set; }
...
}
To perform its serialization, it uses a second class named CacheItem, which simply wraps the initial item you want to cache and the expiration date:
[Serializable]
public class CacheItem
{
public DateTime ExpiryDate;
public object Item;
public CacheItem(object item, DateTime expiryDate)
{
ExpiryDate = expiryDate;
Item = item;
}
}
Now you simply need to override the Add(), Set(), Get(), and Remove() methods. All of these methods receive a key that uniquely identifies the cached content. The key is based on the file name of the cached page. For example, if you use output caching with a page named OutputCaching.aspx in a
web site named CustomCacheProvider, your code might receive a key like this:
a2/customcacheprovider/outputcaching.aspx
To translate this into a valid file name, the code simply replaces slash characters (\) with dashes (-).
It also adds the extension .txt to distinguish this cached content from a real ASP.NET page and to make it easier for you to open it and review its content during debugging. Here’s an example of a transformed file name:
a2-customcacheprovider-outputcaching.aspx.txt
To perform this transformation, the FileOutputCacheProvider uses a private method named ConvertKeyToPath():
private string ConvertKeyToPath(string key)
{
// Flatten it to a single file name, with no path information.
string file = key.Replace('/', '-');
// Add .txt extension so it's not confused with a real ASP.NET file.
file += ".txt";
return Path.Combine(CachePath, file);
}
Other approaches are possible—for example, some caching systems use the types from the System.Security.Cryptography namespace to convert the file name to a unique hash value, which looks like a string of meaningless characters.
Using this method, it’s easy to write the Add() and Set() methods. Remember, the difference between the two is that Set() always stores its content, while Add() must check if it already exists. Add() also returns the cached object. The actual serialization code simply uses the BinaryFormatter to convert the rendered page into a stream of bytes, which can then be written to a file.
public override object Add(string key, object entry, DateTime utcExpiry)
{
// Transform the key to a unique filename.
string path = ConvertKeyToPath(key);
// Set it only if it is not already cached.
if (!File.Exists(path))
{
Set(key, entry, utcExpiry);
}
return entry;
}
public override void Set(string key, object entry, DateTime utcExpiry)
{
CacheItem item = new CacheItem(entry, utcExpiry);
string path = ConvertKeyToPath(key);
// Overwrite it, even if it already exists.
using (FileStream file = File.OpenWrite(path))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(file, item);
}
}
The Get() method is similarly straightforward. However, it must check the expiration date of the retrieved item, and discard it if it has expired:
public override object Get(string key)
{
string path = ConvertKeyToPath(key);
if (!File.Exists(path)) return null;
CacheItem item = null;
using (FileStream file = File.OpenRead(path))
{
BinaryFormatter formatter = new BinaryFormatter();
item = (CacheItem)formatter.Deserialize(file);
}
// Remove expired items.
if (item.ExpiryDate <= DateTime.Now.ToUniversalTime())
{
Remove(key);
return null;
}
return item.Item;
}
Finally, the Remove() method simply deletes the file with the cached data:
public override void Remove(string key)
{
string path = ConvertKeyToPath(key);
if (File.Exists(path)) File.Delete(path);
}
Using a Custom Cache Provider
To use a custom cache provider, you first need to add it inside the <caching> section. Here’s an example that adds the FileCacheProivder and simultaneously sets it to be the default cache provider for all output
caching:
<configuration>
<system.web>
<caching>
<outputCache defaultProvider="FileCache">
<providers>
<add name="FileCache" type="FileCacheProvider" cachePath="~/Cache" />
</providers>
</outputCache>
</caching>
...
</system.web>
</configuration>
This assumes that the FileCacheProvider is a class in the current web application (for example, as a file in the App_Code folder of a projectless web site). If the class were part of a separate assembly, you would need to include the assembly name. For example, a FileCacheProvider in a namespace named CustomCaching and compiled in an assembly named CacheExtensibility would require this configuration:
<add name="FileCache" type="CustomCaching.FileCacheProvider, CacheExtensibility"
cachePath="~/Cache" />
There’s one other detail here. This example includes a custom attribute, named cachePath.
ASP.NET simply ignores this added detail, but your code is free to retrieve it and use it. For example, the FileCacheProvider can use the Initialize() method to read this information and set the path (which, in this case, is a subfolder named Cache in the web application folder).
public override void Initialize(string name, NameValueCollection attributes)
{
base.Initialize(name, attributes);
// Retrieve the web.config settings.
CachePath = HttpContext.Current.Server.MapPath(attributes["cachePath"]);
}
If you don’t use the defaultProvider attribute, it’s up to you to tell ASP.NET when to use its standard in-memory caching service, and when to use a custom cache provider. You might expect to handle this with a directive in the page, but you can’t, simply because caching acts before the page has been retrieved (and, if it’s successful, caching bypasses the page markup altogether).
Instead, you need to override the GetOutputCacheProviderName() method in the global.asax file.This method examines the current request and then returns a string with the name of the cache provider to use while handling this request. Here’s an example that tells ASP.NET to use the FileCacheProvider with the page OutputCaching.aspx (but no other):
public override string GetOutputCacheProviderName(HttpContext context)
{
// Get the page.
string pageAndQuery = System.IO.Path.GetFileName(context.Request.Path);
if (pageAndQuery.StartsWith("OutputCaching.aspx"))
return "FileCache";
else
return base.GetOutputCacheProviderName(context);
}