How to dynamically load partially cached controls

   —   
Once more, I spent a lot of time on an unpleasant Microsoft bug (or feature?). I had to figure out why the dynamically loaded controls which use partial caching (output cache) always share the content with completely different controls loaded with the same method. And I got it working! Read further to learn more, you can save yourselves a lot of time if you have the same trouble ...
Please note that this article is only for experienced developers which know what is partial caching and how the dynamic controls loading works.

Hi there,

It seems like Microsoft bugs (features) are chasing me, I had trouble with TFS not long ago and now I faced another one. While implementing partial cache to our new version, I faced a huge issue.

Some introduction

The issue symptoms are not hard to describe. It happens every time you try to load the user control (or server control), which uses the output caching, dynamically. What happens is that the controls for some reason use the same cache storage (same cache item) and therefore display the same content regardless of how different they are.

When you use following code ...

Controls.Add(LoadControl(typeof(MyCachedControl), null));
Controls.Add(LoadControl(typeof(MyOtherCachedControl), null));

... everything works fine, until you put that loading into your own method, letting the control be loaded every time with the same line of code, like this ...

Control GetControl(Type type)
{
  return LoadControl(type, null); // This is the problematic line
}

void AddControl(Type type)
{
  Controls.Add(GetControl(type));
}

AddControl(typeof(MyCachedControl);
AddControl(typeof(MyOtherCachedControl);

... of course this seems that it is not needed, you could use the first example, unless you implement some component which needs to load the controls based on some parameters where the loading cannot be hardcoded on several lines.

Anyway, what happens is that controls loaded like this display both the same content from the partial cache, which is bad, because each control is completely different so it should be cached separately.

Why is that?

The reason seems simple and when you debug the code, you can find after several minutes (or hours) that the cached controls have the same ID for some reason, and the ID is in the form _90069d08d10e which definitely seems like some automatically assigned unique identifier. After I found this, i got one more key word for the Google and it helped me find a very useful post from a guy named Anders, you can find it over here:

ljusberg.se/blogs/smorakning/archive/2007/10/17/caching-dynamic-controls-in-asp-net-solution.aspx

Which actually says why this happens. The reason is that the ID is based on the last two lines of the stack which is always the same in our case, isn't it? He suggests to create the loading code dynamically and compile it ... but no one actually can afford such thing if he creates the solution which must run on an limited trust level or cannot compile the code at runtime, right?

There was also one solution which seemed fine, but hardly final as it is presented by the user MoTiOnXml in this thread:

http://forums.asp.net/t/125051.aspx

Creating the "proxy" parent control for each of the controls works, but who wants to force the customers to do that for their controls? Isn't actually the hardcoding of the  LoadControl method simpler than that?

That's why we have to use other solution, something that allows us to load the controls with different stacks, based on some parameter ... that's where my contribution is.

Behold! The final and ultimate solution is here!

... at least until it will be fixed by Microsoft guys ;-)

Based on the information I had, it was clear that the solution has to route through some lines of code differently for each loaded control, that's why we will need to use the construction like:

private static Control LoadCachedControl(Page page, Type type, object[] parameters, int i)
{
   if (i-- <= 0) { return page.LoadControl(type, parameters); }
   if (i-- <= 0) { return page.LoadControl(type, parameters); }
   if (i-- <= 0) { return page.LoadControl(type, parameters); }
   ...

   return page.LoadControl(type, parameters);
}

Where "i" will be the number of control I am loading. At this moment, we can say that this solution can handle as many unique controls as many of such lines is there. Who wants to have 1k lines of code to be able to load 1000 different controls? ... Certainly NOT me!

That's where we need to think a little bit more ... they said 2 lines of stack, but we are using only one, right? (our method LoadCachedControl).
 

What if there was a method LoadCachedControl1 which could choose from 10 possible options (lines) of calling our LoadCachedControl method which would be now named LoadCachedControl2 and could load the controls in 10 different lines? That makes 10 x 10 = 100 combinations of stack with "only" 20 lines of code, right? If I had 32 x 32 = 1024, I am already above the previous solution with "only" 64 lines of code instead of original 1000 lines that I can get from a single method. Of course I can go further, I am able to support 1M of combinations if I have 2k lines of code, but who needs so many cached controls ... probably noone. That's why my solution handles 10200 possible options using 100 x 100 lines for each method plus some additional options provided by the code at the end of each methods (I can have unique stack if I call the level 2 method twice in a row).

Not exactly all

So, we have the solution which gives us the control loaded with unique ID based on the unique index (parameter "i"). The only thing left is to ensure that my controls always use the same index so they can be properly cached. It is just a simple Hashtable on top of these methods, which saves the control indexes based on some unique string ID, which can be the control ID, some GUID, or anything else you can use touniquely identify the instance of the control.

The only thing left ... the code

Here is my final code you can use to load your partially cached controls, it supports up to 10200 uniquely cached controls and you can even extend it to more ...

Code of PartialHelper

Just call PartialHelper.LoadCachedControl(page, type, parameters, controlId); and the helper ensures the rest. You can do the same with the version of LoadControl with path, we didn't need it so the helper doesn't include it. It should work the same way.

... and no, there is no VB version so translate it to VB by yourselves ;-)
Share this article on   LinkedIn

Martin Hejtmanek

Hi, I am the CTO of Kentico and I will be constantly providing you the information about current development process and other interesting technical things you might want to know about Kentico.

Comments

MartinH commented on

Hi Marcelo,

The file should now be available without extension (just add .cs extension to it), due to changed security policy the .cs files are currently not allowed for download. The team is looking at ways how to make .cs files available without compromising security of this portal.

Marcelo commented on

@martinh_kentico There was a link to "PartialCacheHelper.cs" and the file is not available. Could you help by sending me the file? I'm having this same problem.

Please, if yes, send me to "marcelo.celo{{at}}gmail.com"

Thanks.

Tommy commented on

I managed to come around this problem using a DynamicMethod. Full description and code can be found in the blog entry I wrote after I solved it:

http://www.tommycode.se/2012/04/outputcache-on-dynamically-loaded.html

Martin Hejtmanek commented on

Hi Kevin, the dependencies are actually not SqlDependencies, they are standard in-memory cache dependencies, the distribution in a web farm environment is solved by our web farm module. We will probably add support for your own cache provider or providers for other solutions for cache in some of the next version.

Kevin Clark commented on

ASP.NET Cache being stand-alone & InProc is a problem. Trying to "simulate" distribution through SqlCacheDependency is really a "hack" in my opinion. The right way to solve a scalability problem is through an in-memory distributed cache.
And, Microsoft is finally realizing it as well.

I personally like NCache which is really impressive because of its rich set of caching topologies (Mirrored, Replicated, Partitioned, Partition-Replica, and Client Cache). The cool thing is that it also has NCache Express which is free for 2-server environments.