0

I have implement a simple file-based custom OutputCacheProvider based on samples i found on the Internet.

The code follows:

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();
        }
    }
}

I have then created an .aspx with the header

<%@ OutputCache Duration="3600" Location="Server" VaryByParam="*" %>

I have changed the default output cache provider to my web.config to mine.

The strange behavior is that the page is not cached. Instead this is a sample output from my debugging information. It seems that:

  1. The page is retrieved from tha cache and sent back to ASP.Net
  2. Right after that ASP.Net calls the Remove() method to my page
  3. Finally ASP.Net calls Set() and the page is updated - no effective caching

    Get: Querying 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) retrieved.

    Get: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt) retrieved and used

    Get: Querying 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) retrieved.

    Get: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) retrieved and used

    Remove: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) requested.

    Remove: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) executed.

    ADD: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt)

    ADD: a2/ss2.aspx (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\7394fd15241e5b7f5c437ddf28dcd0e5.txt) already exists. Will be returned.

    Set: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt) requested.

    Set: a2/ss2.aspxHQFCNmycustom2VDE (C:\eShopKey\ASP.Net\Shops\myshoe_dev\Cache\3e72454ab3f36e4cfe3964e5063be622.txt): 30/05/2012 15:07:27 saved to disk.

So my questions:

  1. Why ASP.Net keeps invalidating my page?
  2. When Remove() and Set() methods are called by ASP.Net? I have not found any info regarding that.
  3. If i rename the page and use this variation caching works! This is totally weird.

Note that if i use the default ASP.Net outputcacheprovider caching works as expected.


I found what is going on but unable to fix it:

Let's say i open the page: http://www.mydomain.com/mypage.aspx?param1=1

ASP.Net sends 2 consecutive GET requests to my OutputCacheProvider:

  • one for the page mypage.aspx
  • another for the same page but with the querystring parameters attached

It seems to me that the first request is somehow related with the second one, like a header.

As soon as i call consecutively the same page, with the same querystring, caching working as expected.

If i call next the page: http://www.mydomain.com/mypage.aspx?param1=2

then the same, 2 step GET sequence, is initialized. The ASP.Net sends 2 GET requests, one for the page without parameters and one with parameters.

The first GET request (for the page without parameters) is then found on the cache and returned back to ASP.Net. But somehow is unrelated with the second one. It is related to the first variation of the call (param1=1).

So, nevertheless if the second request has been cached before, ASP.Net thinks that the cached page is invalid and ask again for add / set.

To summarize it seems that you can have just one variation of the page to the cache at a given moment. All previous cahed variations will be invalidated as the page will be called again with other parameters.

There is no way to check what the first GET request is related to as ASP.NET uses the same key to retrieve it.

So my new questions:

  • Why ASP.Net sends 2 requests for every page to the custom output cache provider? Does anybody knows?
  • How i can overcome this strange behavior?
  • Is the AspNetInternalProvider has the same behavior?
Tim Post
  • 33,371
  • 15
  • 110
  • 174
zissop
  • 31
  • 2

2 Answers2

1

I found the solution! The problem was on Add method. It has to be written on all providers like below:

public override object Add(string key, object entry, DateTime utcExpiry) {
        String vKey = TransformKey(key);

        object res = Get(key);
        if (res == null) {
            Set(key, entry, utcExpiry);
            return entry;
        }

        return res;
    }

The TransformKey method just returns a safe string (string without bad characters) based on key (for example the MD5 hash of the key). Look for an implementation on my first posted code.

zissop
  • 31
  • 2
  • Congrats on the fix! When you are able, please make sure to mark your answer as 'accepted' so that others will be able to learn from your success. Cheers~ – Andrew Kozak Jun 04 '12 at 16:01
0

the first request returns an object System.Web.Caching.CachedVary, and second request returns System.Web.Caching.OutputCacheEntry. According to the name of object, the first one is for OutputCache, and the second one is for the data of page.

if you have any questions, pls send email to shengzhengshan@hotmail.com

Hope it can help you!

Amir Sheng

Botz3000
  • 39,020
  • 8
  • 103
  • 127
  • So what is the required actions I have to take in order the OutputCacheProvider to work as expected? Note that every single custom OutputCacheProvider i have found on the Internet suffers from the same problem. – zissop Jun 01 '12 at 12:31