Saturday, August 20, 2011

MVC Donut hole caching for Razor View

    I was interested with Donut caching for a while and today I have a need for the Donut hole caching or partial view caching. As many of us, I tried to find whether some smart guys had solved this problem. And it seems like Mr.Haacked had mentioned this in his post. However, that solution is for ASP.NET view engine. I have no choice and have to solve it myself. Hopefully MVC team will support this feature in next versions of MVC framework. Hmmm indeed, it could be supported in version 4: See road map.


    My idea is if a view engine renders the view's content to a stream or text writer, we can intercept that process, cache the output string to somewhere together with the view's id or what ever that can identify the view. Then next time the view is rendered, we can determine whether or not to get the content from cache or let the view engine continue it's job. So i dig into the MVC source code and find this:

public class RazorView : BuildManagerCompiledView
{
    protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
    {
        //.............
    }
}
    That's exactly what i need, isn't it? We probably can create a derived class from RazorView and override above method, get the content that the text writer receive and write it directly to the writer if the view is cached. So, i need a custom text writer that could return to me what it receive :D
public class TrackableTextWriter : TextWriter
{
    private readonly TextWriter _writer;
    private StringBuilder _mem;
    public TrackableTextWriter(TextWriter writer)
    {
        _writer = writer;
        _mem = new StringBuilder();
    }
    public override Encoding Encoding
    {
        get { return _writer.Encoding; }
    }
    public override void Write(string value)
    {
        _writer.Write(value);
        _mem.Append(value);
    }
    public string GetWrittenString()
    {
        return _mem.ToString();
    }
    protected override void Dispose(bool disposing)
    {
        if (!disposing)
        {
            _writer.Dispose();           
        }
        base.Dispose(disposing);
        _mem = null;
    }
}
    Alright, now it's time to create a derived of RazorView:
public class CachableRazorView : RazorView
{
	private const string ViewCachePrefix = "ViewCache__";
    protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
	{
		var appCache = viewContext.HttpContext.Cache;
		var cacheKey = ViewCachePrefix + ViewPath;
		// Check if there was a Cache config that had been fully added to the cache
		var cacheConfiguration = appCache[cacheKey] as CacheConfiguration;
		if (cacheConfiguration != null && cacheConfiguration.Data != null)
		{
			writer.Write(cacheConfiguration.Data);
			return;
		}
		var trackableTextWriter = new TrackableTextWriter(writer);
		base.RenderView(viewContext, trackableTextWriter, instance);
		
		// Cache config has just been added when the view is rendered the first time thanks to the HtmlHelper
		cacheConfiguration = appCache[cacheKey] as CacheConfiguration;
		if (cacheConfiguration != null)
		{
			var writtenString = trackableTextWriter.GetWrittenString();
			cacheConfiguration.Data = writtenString;
			appCache.Remove(cacheConfiguration.Id);
			appCache.Add(cacheConfiguration.Id,
						cacheConfiguration,
						null,
						Cache.NoAbsoluteExpiration,
						TimeSpan.FromSeconds(cacheConfiguration.Duration),
						CacheItemPriority.Default,
						null);
		}
	}
}
    Then there's of course a place we can return this custom view instead of RazorView. That's definitely the RazorViewEngine. So next step is creating our custom razor view engine:
public class CachableRazorViewEngine : RazorViewEngine
{
	protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
	{
		return new CachableRazorView(controllerContext, partialPath, null, false, FileExtensions, ViewPageActivator);
	}

	protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
	{
		return new CachableRazorView(controllerContext, viewPath, masterPath, true, FileExtensions, ViewPageActivator);
	}
}
    As usual, we need to make the web application use this view engine by modifying the globals.asax
protected void Application_Start()
{
	AreaRegistration.RegisterAllAreas();

	RegisterGlobalFilters(GlobalFilters.Filters);

	ViewEngines.Engines.Clear();
	ViewEngines.Engines.Add(new CachableRazorViewEngine());

	RegisterRoutes(RouteTable.Routes);
}
    Using this approach, the traditional OutputCache will not have value. So the idea pops in my head is making a HtmlHelper extension and calling it in the view, something like this:
@{
	Html.OutputCache(new CacheConfiguration { Duration = 10 });
}
    So here is the implementation:
public static class CacheHtmlHelperExtensions
{
	public const string ViewCachePrefix = "ViewCache__";
	public static void OutputCache(this HtmlHelper helper, CacheConfiguration cacheConfiguration)
	{
		var view = helper.ViewContext.View as BuildManagerCompiledView;
		if (view != null)
		{
			cacheConfiguration.Id = ViewCachePrefix + view.ViewPath;
			helper.ViewContext.HttpContext.Cache.Add(cacheConfiguration.Id,
													cacheConfiguration,
                                                    null,
                                                    Cache.NoAbsoluteExpiration,
                                                    TimeSpan.FromSeconds(cacheConfiguration.Duration),
                                                    CacheItemPriority.Default,
                                                    null);
		}
	}
}
    Okey, it's time to test this implementation. I put the Html.OutputCache method in the Index page of default MVC application like above. So the view will be cached in 10 seconds. If the view is not accessed in next 10 seconds, the cache engine will remove the content from the cache. However, within 10 seconds, if the view is accessed again, the cache will remain for next 10 seconds and so on. Pretty cool, huh? I need to run the performance test on this page to see the actual result. There is a very cool tool in Linux named "curl-loader" but I didn't know any for Windows. After a while searching, I found apache benchmark very similar and usefull :D. I use "ab" tool to test the web app when enable and disable the cache. The result is very interesting. Eventhough the Index page is quite simple with only text, no database access but when I enable the cache, the web server can serve 1107 requests per second when I run 10000 requests to server at concurency level at 100 compare to only 531 requests per second when I disable the cache, 2 times faster:

benchmark result

    Summary, there is a outstanding issue using this approach, the builtin output cache of ASP.NET can not be utilised. It also ignores the value of ViewModel, that's mean this implementation has not supported caching by different view model. But I think we can do it if we really need that feature. Just find a way to distinguish the different between view model values, it could be a hash code or something :D.

Source code: Razor.DonutHoleCaching.zip

Cheers

14 comments:

Tugberk said...

Hmm, looks like a nice post. To be honest, I haven't read all of it but as I see, you have gone wild a little bit. Why not create a child action and output it inside the view with Html.Action HtmlHelper? This will make it easier.

vantheshark said...

Haha, you are right. ASP.NET MVC is so easy to extend that sometime we pick the wrong technical decision :D.

Anyway, there are still cases to use this approach if someone like me don't like to let the view to be aware of the controller :D. It's an anti pattern, isn't it?

Sagar said...

Nice post.
In my search results, found another very nice article on Donut Hole Caching in MVC.
The article also include tutorial for it.
Do try it http://swsharinginfo.blogspot.in/2012/03/donut-hole-caching-partial-view-caching.html

mary Brown said...

I have read your blog its very attractive and impressive. I like it your blog.

ASP.NET MVC Training in Chennai | ASP.NET MVC Training in Chennai

ASP.NET MVC Online Training | Online LINQ Training

Ramya Krishnan said...

i have go through your article, it is very nice and more informative, clearly understands me. Thanks.
ASP.NET Training in chennai

Revathy A said...

A very nice guide. I will definitely follow these tips. Thank you for sharing such detailed article. I am learning a lot from you.
angularjs Training in marathahalli

angularjs interview questions and answers

angularjs Training in bangalore

angularjs Training in bangalore

angularjs online Training

angularjs Training in marathahalli

simbu said...

A very nice guide. I will definitely follow these tips. Thank you for sharing such detailed article. I am learning a lot from you.
Java training in Chennai | Java training in Velachery

Java training in Chennai | Java training in Omr

Oracle training in Chennai

Java training in Chennai | Java training in Annanagar

thulasi ragini said...

Just stumbled across your blog and was instantly amazed with all the useful information that is on it. Great post, just what i was looking for and i am looking forward to reading your other posts soon!
python training in rajajinagar
Python training in bangalore
Python training in usa

kevin antony said...

This is most informative and also this post most user friendly and super navigation to all posts... Thank you so much for giving this information to me.

rpa training in chennai
rpa training in bangalore
rpa course in bangalore
best rpa training in bangalore
rpa online training

Nila shri said...

Thanks for the informative article. This is one of the best resources I have found in quite some time. Nicely written and great info. I really cannot thank you enough for sharing.
Data Science training in Chennai | Data Science Training Institute in Chennai
Data science training in Bangalore | Data Science Training institute in Bangalore
Data science training in pune | Data Science training institute in Pune
Data science online training | online Data Science certification Training-Gangboard
Data Science Interview questions and answers

johnsy sai said...

Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.
Best Devops Training in pune
excel advanced excel training in bangalore
Devops Training in Chennai

vijay antony said...

I am really happy with your blog because your article is very unique and powerful for new reader.
Click here:
selenium training in chennai | selenium course in chennai
selenium training in bangalore | selenium course in bangalore
selenium training in Pune | selenium course in pune | selenium class in pune
selenium training in Pune | selenium course in pune | selenium class in pune
selenium online training | selenium training online | online training on selenium


sandy star said...

Hello! This is my first visit to your blog! We are a team of volunteers and starting a new initiative in a community in the same niche. Your blog provided us useful information to work on. You have done an outstanding job.
Best AWS Training in Chennai | Amazon Web Services Training in Chennai
AWS Training in Bangalore | Amazon Web Services Training in Bangalore
Amazon Web Services Training in Pune | Best AWS Training in Pune

Safety Professionals said...

I have to voice my passion for your kindness giving support to those people that should have guidance on this important matter.
iosh course in chennai

Post a Comment