Yep, it's possible to programmatically enumerate and remove items from IIS's kernel cache.
Caveats:
- non-trivial text parsing requred for enumeration
- lots of ugly P/Invoke required for removal
- Also, you'll need at least Medium Trust (and probably Full Trust) to do the things below.
- Removal won't work in IIS's integrated pipeline mode.
- Enumeration probably won't work on IIS6
Enumeration:
The only documented way I know to enumerate the IIS kernel cache is a command-line app available in IIS7 and above (although you might be able to copy the NETSH helper DLL from V7 onto a V6 system-- haven't tried it).
netsh http show cachestate
See MSDN Documentation of the show cachestate command for more details. You could turn this into an "API" by executing the process and parsing the text results.
Big Caveat: I've never seen this command-line app actually return anything on my server, even for apps running in Classic mode. Not sure why-- but the app does work as I can see from other postings online. (e.g. http://chrison.net/ViewingTheKernelCache.aspx)
If you're horribly allergic to process creation and feeling ambitious, NETSH commands are implemented by DLL's with a documented Win32 interface, so you could write code which pretends it's NETSH.exe and calls into IIS's NETSH helper DLL directly. You can use the documentation on MSDN as a starting point for this approach. Warning: impersonating NETSH is non-trivially hard since the interface is 2-way: NETSH calls into the DLL and the DLL calls back into NETSH. And you'd still have to parse text output since the NETSH interface is text-based, not object-based like PowerShell or WMI. If it were me, I'd just spawn a NETSH process instead. ;-)
It's possible that the IIS7 PowerShell snapin may support this functionality in the future (meaning easier programmatic access than the hacks above), but AFAIK only NETSH supports this feature today.
Invalidation:
I've got good news and bad news for you.
The good news: Once you know the URL of the item you want to yank from IIS's kernel cache, there's a Win32 API available to remove it on IIS6 and above. This can be called from C# via P/Invoke (harder) or by putting the call in a managed C++ wrapper DLL. See HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK on MSDN for details.
I took a stab at the code required (attached below). Warning: it's ugly and untested-- it doesn't crash my IIS but (see above) I can't figure out how to get cache enumeration working so I can't actually call it with a valid URL to pull from the cache. If you can get enumeration working, then plugging in a valid URL (and hence testing this code) should be easy.
The bad news:
- as you can guess from the code sample, it won't work on IIS7's integrated pipeline mode, only in Classic mode (or IIS6, of course) where ASP.NET runs as an ISAPI and has access to ISAPI functions
- messing with private fields is a big hack and may break in a new version
- P/Invoke is hard to deal with and requires (I believe) full trust
Here's some code:
using System;
using System.Web;
using System.Reflection;
using System.Runtime.InteropServices;
public partial class Test : System.Web.UI.Page
{
/// Return Type: BOOL->int
public delegate int GetServerVariable();
/// Return Type: BOOL->int
public delegate int WriteClient();
/// Return Type: BOOL->int
public delegate int ReadClient();
/// Return Type: BOOL->int
public delegate int ServerSupportFunction();
/// Return Type: BOOL->int
public delegate int EXTENSION_CONTROL_BLOCK_GetServerVariable();
/// Return Type: BOOL->int
public delegate int EXTENSION_CONTROL_BLOCK_WriteClient();
/// Return Type: BOOL->int
public delegate int EXTENSION_CONTROL_BLOCK_ReadClient();
/// Return Type: BOOL->int
public delegate int EXTENSION_CONTROL_BLOCK_ServerSupportFunction();
public static readonly int HSE_LOG_BUFFER_LEN = 80;
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
public struct EXTENSION_CONTROL_BLOCK
{
/// DWORD->unsigned int
public uint cbSize;
/// DWORD->unsigned int
public uint dwVersion;
/// DWORD->unsigned int
public uint connID;
/// DWORD->unsigned int
public uint dwHttpStatusCode;
/// CHAR[HSE_LOG_BUFFER_LEN]
[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 80 /*HSE_LOG_BUFFER_LEN*/)]
public string lpszLogData;
/// LPSTR->CHAR*
public System.IntPtr lpszMethod;
/// LPSTR->CHAR*
public System.IntPtr lpszQueryString;
/// LPSTR->CHAR*
public System.IntPtr lpszPathInfo;
/// LPSTR->CHAR*
public System.IntPtr lpszPathTranslated;
/// DWORD->unsigned int
public uint cbTotalBytes;
/// DWORD->unsigned int
public uint cbAvailable;
/// LPBYTE->BYTE*
public System.IntPtr lpbData;
/// LPSTR->CHAR*
public System.IntPtr lpszContentType;
/// EXTENSION_CONTROL_BLOCK_GetServerVariable
public EXTENSION_CONTROL_BLOCK_GetServerVariable GetServerVariable;
/// EXTENSION_CONTROL_BLOCK_WriteClient
public EXTENSION_CONTROL_BLOCK_WriteClient WriteClient;
/// EXTENSION_CONTROL_BLOCK_ReadClient
public EXTENSION_CONTROL_BLOCK_ReadClient ReadClient;
/// EXTENSION_CONTROL_BLOCK_ServerSupportFunction
// changed to specific signiature for invalidation callback
public ServerSupportFunction_HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK ServerSupportFunction;
}
/// Return Type: BOOL->int
///ConnID: DWORD->unsigned int
///dwServerSupportFunction: DWORD->unsigned int
///lpvBuffer: LPVOID->void*
///lpdwSize: LPDWORD->DWORD*
///lpdwDataType: LPDWORD->DWORD*
[return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)]
public delegate bool ServerSupportFunction_HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK(
uint ConnID,
uint dwServerSupportFunction, // must be HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK
out Callback_HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK lpvBuffer,
out uint lpdwSize,
out uint lpdwDataType);
public readonly uint HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK = 1040;
// typedef HRESULT (WINAPI * PFN_HSE_CACHE_INVALIDATION_CALLBACK)(WCHAR *pszUrl);
[return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)]
public delegate bool Callback_HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK(
[MarshalAs(UnmanagedType.LPWStr)]string url);
object GetField (Type t, object o, string fieldName)
{
FieldInfo fld = t.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
return fld == null ? null : fld.GetValue(o);
}
protected void Page_Load(object sender, EventArgs e)
{
// first, get the ECB from the ISAPIWorkerRequest
var ctx = HttpContext.Current;
HttpWorkerRequest wr = (HttpWorkerRequest) GetField(typeof(HttpContext), ctx, "_wr");
IntPtr ecbPtr = IntPtr.Zero;
for (var t = wr.GetType(); t != null && t != typeof(object); t = t.BaseType)
{
object o = GetField(t, wr, "_ecb");
if (o != null)
{
ecbPtr = (IntPtr)o;
break;
}
}
// now call the ECB callback function to remove the item from cache
if (ecbPtr != IntPtr.Zero)
{
EXTENSION_CONTROL_BLOCK ecb = (EXTENSION_CONTROL_BLOCK)Marshal.PtrToStructure(
ecbPtr, typeof(EXTENSION_CONTROL_BLOCK));
uint dummy1, dummy2;
Callback_HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK invalidationCallback;
ecb.ServerSupportFunction(ecb.connID,
HSE_REQ_GET_CACHE_INVALIDATION_CALLBACK,
out invalidationCallback,
out dummy1,
out dummy2);
bool success = invalidationCallback("/this/is/a/test");
}
}
}