インターネットで見つけたサンプルに基づいて、単純なファイルベースのカスタム OutputCacheProvider を実装しました。
コードは次のとおりです。
using System;
using System.Configuration;
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Diagnostics;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
namespace SimpleCachedProvider
{
public class FileCacheProvider : OutputCacheProvider {
private string _cachePath;
void WriteToFile(String filename, String contents) {
FileStream fs = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write);
StreamWriter w = new StreamWriter(fs, System.Text.Encoding.GetEncoding(1253));
w.BaseStream.Seek(0, SeekOrigin.Begin);
w.BaseStream.SetLength(0);
w.Write(contents);
w.Flush();
w.Close();
}
void AppendToFile(String filename, String contents) {
if (contents.ToLower().IndexOf("ss2.aspx") >= 0 || contents.ToLower().IndexOf("default.aspx") >= 0) {
FileStream fs = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write);
StreamWriter w = new StreamWriter(fs, System.Text.Encoding.GetEncoding(1253));
w.BaseStream.Seek(0, SeekOrigin.End);
w.Write(contents);
w.Flush();
w.Close();
}
}
private string CachePath {
get {
if (!string.IsNullOrEmpty(_cachePath))
return _cachePath;
_cachePath = ConfigurationManager.AppSettings["OutputCachePath"];
var context = HttpContext.Current;
if (context != null) {
_cachePath = context.Server.MapPath(_cachePath);
if (!_cachePath.EndsWith("\\"))
_cachePath += "\\";
}
return _cachePath;
}
}
public override object Add(string key, object entry, DateTime utcExpiry) {
var path = GetPathFromKey(key);
AppendToFile(CachePath + "info.txt", "ADD: " + key + " (" + path + ")\r\n");
if (File.Exists(path)) {
AppendToFile(CachePath + "info.txt", "ADD: " + key + " (" + path + ") already exists. Will be returned.\r\n");
return entry;
}
AppendToFile(CachePath + "info.txt", "ADD: " + key + " (" + path + ") does not exists. Will be created.\r\n");
using (var file = File.OpenWrite(path)) {
var item = new CacheItem { Expires = utcExpiry, Item = entry };
var formatter = new BinaryFormatter();
formatter.Serialize(file, item);
AppendToFile(CachePath + "info.txt", "ADD: " + key + " (" + path + ") saved to disk.\r\n");
}
return entry;
}
public override void Set(string key, object entry, DateTime utcExpiry) {
var item = new CacheItem { Expires = utcExpiry, Item = entry };
var path = GetPathFromKey(key);
AppendToFile(CachePath + "info.txt", "Set: " + key + " (" + path + ") requested.\r\n");
using (var file = File.OpenWrite(path)) {
var formatter = new BinaryFormatter();
formatter.Serialize(file, item);
AppendToFile(CachePath + "info.txt", "Set: " + key + " (" + path + "): " + utcExpiry.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss") + " saved to disk.\r\n");
}
}
public override object Get(string key) {
var path = GetPathFromKey(key);
AppendToFile(CachePath + "info.txt", "Get: Querying " + key + " (" + path + ")\r\n");
if (!File.Exists(path)) {
AppendToFile(CachePath + "info.txt", "Get: " + key + " (" + path + ") not found.\r\n");
return null;
}
CacheItem item = null;
using (var file = File.OpenRead(path)) {
var formatter = new BinaryFormatter();
item = (CacheItem)formatter.Deserialize(file);
AppendToFile(CachePath + "info.txt", "Get: " + key + " (" + path + ") retrieved.\r\n");
}
if (item == null || item.Expires <= DateTime.Now.ToUniversalTime()) {
AppendToFile(CachePath + "info.txt", "Get: " + key + " (" + path + ") deleted due to expiration.\r\n");
Remove(key);
return null;
}
AppendToFile(CachePath + "info.txt", "Get: " + key + " (" + path + ") retrieved and used\r\n");
return item.Item;
}
public override void Remove(string key) {
var path = GetPathFromKey(key);
AppendToFile(CachePath + "info.txt", "Remove: " + key + " (" + path + ") requested.\r\n");
if (File.Exists(path)) {
AppendToFile(CachePath + "info.txt", "Remove: " + key + " (" + path + ") executed.\r\n");
File.Delete(path);
}
}
private string GetPathFromKey(string key) {
return CachePath + MD5(key) + ".txt";
}
private string MD5(string s) {
MD5CryptoServiceProvider provider;
provider = new MD5CryptoServiceProvider();
byte[] bytes = Encoding.UTF8.GetBytes(s);
StringBuilder builder = new StringBuilder();
bytes = provider.ComputeHash(bytes);
foreach (byte b in bytes)
builder.Append(b.ToString("x2").ToLower());
return builder.ToString();
}
}
}
次に、ヘッダーを含む .aspx を作成しました
<%@ OutputCache Duration="3600" Location="Server" VaryByParam="*" %>
デフォルトの出力キャッシュ プロバイダーを自分の web.config に変更しました。
奇妙な動作は、ページがキャッシュされていないことです。代わりに、これは私のデバッグ情報からの出力例です。のようだ:
- ページはキャッシュから取得され、ASP.Net に送り返されます。
- その直後、ASP.Net は私のページに Remove() メソッドを呼び出します
最後に ASP.Net が Set() を呼び出し、ページが更新されます - 効果的なキャッシュはありません
取得: a2/ss2.aspx のクエリ (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt)
取得: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt) を取得しました。
Get: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt) 取得して使用
取得: a2/ss2.aspxHQFCNmycustom2VDE のクエリ (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt)
取得: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) を取得しました。
Get: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) 取得して使用
削除: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) が要求されました。
削除: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) が実行されました。
追加: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt)
追加: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt) は既に存在します。返されます。
セット: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) が要求されました。
セット: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt): 30/05/2012 15:07:27 がディスクに保存されました。
だから私の質問:
- ASP.Net がページを無効にし続けるのはなぜですか?
- ASP.Net によって Remove() および Set() メソッドが呼び出されるのはいつですか? それに関する情報は見つかりませんでした。
- ページの名前を変更してこのバリエーションを使用すると、キャッシュが機能します! これはまったく奇妙です。
デフォルトの ASP.Net outputcacheprovider を使用すると、キャッシングが期待どおりに機能することに注意してください。
何が起こっているのかを見つけましたが、修正できませんでした:
ページを開いたとしましょう: http://www.mydomain.com/mypage.aspx?param1=1
ASP.Net は 2 つの連続した GET 要求を OutputCacheProvider に送信します。
- ページ mypage.aspx 用の 1 つ
- 同じページの別のものですが、クエリ文字列パラメーターが添付されています
ヘッダーのように、最初のリクエストが 2 番目のリクエストと何らかの形で関連しているように思えます。
同じクエリ文字列を使用して同じページを連続して呼び出すとすぐに、キャッシングが期待どおりに機能します。
次のページを呼び出す場合: http://www.mydomain.com/mypage.aspx?param1=2
次に、同じ 2 ステップの GET シーケンスが初期化されます。ASP.Net は 2 つの GET 要求を送信します。1 つはパラメーターなしのページ用で、もう 1 つはパラメーター付きです。
次に、最初の GET 要求 (パラメーターのないページに対する) がキャッシュで検出され、ASP.Net に返されます。しかし、どういうわけか2番目のものとは無関係です。これは、呼び出しの最初のバリエーション (param1=1) に関連しています。
それにもかかわらず、2 番目の要求が以前にキャッシュされていた場合、ASP.Net はキャッシュされたページが無効であると判断し、追加/設定を再度要求します。
要約すると、特定の瞬間にページのバリエーションを 1 つだけキャッシュに入れることができるようです。ページが他のパラメーターで再度呼び出されるため、以前にキャッシュされたすべてのバリエーションは無効になります。
ASP.NET は同じキーを使用して取得するため、最初の GET 要求が何に関連しているかを確認する方法はありません。
だから私の新しい質問:
- ASP.Net がページごとに 2 つの要求をカスタム出力キャッシュ プロバイダーに送信するのはなぜですか? 誰か知っていますか?
- この奇妙な動作をどのように克服できますか?
- AspNetInternalProvider の動作は同じですか?