Thursday, April 12, 2012

OutputCache on dynamically loaded UserControls

Yesterday I ran into an old bad bug in ASP.NET. When I put two controls with OutputCache set on the same page they rendered exactly the same - even tho they were different. The caching simply took one of them and showed on both places.

First I thought that is was an expected behaviour and that i needed to add some argument to the OutputCache directive, like VaryByControl or VaryByCustom, but I never managed to get that to work... I asked Google for help but couldn't really find any help. I did how ever find that the documentation said that adding two controls to the same page should automatically make them cache separately if not "Shared" was set to true in OutputCache.

That piece of information made me dig in another hole... Maybe it had to do with the fact that these controls were added dynamically, using Page.LoadControl and then added to a placeholder with ControlCollection.Add. I created a very simple test where Default.aspx had a placeholder in the body, and in OnInit I added two controls that only rendered a Guid - and had OutputCache. Adding them one by one didn't cause the bug, but when I added them in a loop it did! Wow!

Now that I knew what was the real problem i managed to get better answers from Google!

The first trace is as old as 2003, including hopes for it to be fixed in .NET 1.1 and .NET 2.0. The workaround suggested back then was to create a wrapping control without OutputCache that was the one that was dynamically loaded, and that had a static reference to the control you want to cache.

I wasn't really happy with that solution though and looked further. I found someone on StackOverflow that had the same problem and a reply leading to another solution, where you make sure that the stack is different on all calls. This would work around the problem, because the core of the problem appears to be that the Control you create with LoadControl is named by some hashkey from the callstack. That explains why it worked when I added the control on two separate lines in my test project!

I made a little test with a switch statement where every case did a LoadControl... but on separate rows. And it worked! Showing it to my collegaue Martin we started thinking about other solutions. First we did a recursion version, where we saved current recursion in Page.Items and made another recursion for every subsequent call. That worked too. Then we came to think of anonymous methods, but that didn't do the trick. Maybe they're named in the same way?

Instead we looked at DynamicMethod, which is the way we ended up using. At first I thought it would be bad for performance, but after implemention I did some profiling - and it was only half a millisecond per call extra! Here's the code if you'd like to solve the problem yourself:

public static class CacheFixer {
    private delegate Control LoadControlDelegate(TemplateControl page, string virtualPath);
    private static readonly Dictionary DelegateStore = new Dictionary();

    public static Control LoadControlWithCachingAllowed(TemplateControl page, string virtualPath, string key) {
        if (!DelegateStore.ContainsKey(key)) {
            DelegateStore[key] = CreateLoadControlDelegate();
        }
        return DelegateStore[key](page, virtualPath);
    }

    private static LoadControlDelegate CreateLoadControlDelegate() {
        var dynamicMethod = CreateDynamicMethod();
        return (LoadControlDelegate) dynamicMethod.CreateDelegate(typeof (LoadControlDelegate));
    }

    private static DynamicMethod CreateDynamicMethod() {
        var dynamicMethod = new DynamicMethod("", typeof(Control), new[] { typeof(TemplateControl), typeof(string) });
        var loadControlMethod = typeof (CacheFixer).GetMethod("LoadControlX");
        var ilGenerator = dynamicMethod.GetILGenerator();
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Ldarg_1);
        ilGenerator.Emit(OpCodes.Call, loadControlMethod);
        ilGenerator.Emit(OpCodes.Ret);
        return dynamicMethod;
    }

    public static Control LoadControlX(TemplateControl page, string virtualPath) {
        return page.LoadControl(virtualPath);
    }
}
Post a Comment