Areas in ASP.NET MVC
It wasn’t long after I started using ASP.NET MVC that I realized I needed to be able to split functionality based on specific sections of my site (admin, user, etc…). I wanted a separate set of controllers, models, and views for each section, because each section would be working with the same data but in vastly different ways. A quick google search led me to posts by Phil Haack and Steve Sanderson, and the resulting AreaViewEngine class derived from their code worked well, with one major issue: performance.
As I was developing a site for a sweepstakes for a nationally syndicated TV show by a very famous TV/media personality, the site needed to perform incredibly well. Profiling of the application during development revealed serious problems with the VirtualPathProviderViewEngine as documented here at stackoverflow.com. It seemed no matter what code existed in my app, the highest consumer of CPU time was the FindPartialView method.
Some research led me to the question of view resolution caching documented here but the problem was, I was profiling in release mode. How could it be that the view resolution was being cached and still causing such a problem? I dug into the code (downloaded from CodePlex) and confirmed that the cache should have been enabled, but still, the performance problem persisted. I then spent 3 days drilling into the problem and came up with two improvements that almost nullified the impact of partial view resolution.
- If the view name passed to the view engine starts with “~/”, assume an absolute path and skip area view resolution altogether.
- Avoid the slower resolution of views by the VirtualPathProviderViewEngine and resolve it myself.
The resulting code is below. Testing on my local machine revealed that the enhancements noted above (and commented in the code) increased my requests per second from ~30 to ~110 (a 350% improvement)! On our production environment, testing a static page with no database access, this code allowed us to approach 2000 requests per second using a Zeus ZXTM LB (v5.1) with 4 Windows 2003 web server nodes.
2009/06/19: Alexander reported a missing slash in the path name formatting. Good catch! The code has been updated to reflect the change.
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
namespace ElserInteractive.Framework.Web.Mvc
{
public class AreaViewEngine : WebFormViewEngine
{
public AreaViewEngine()
: base()
{
ViewLocationFormats = new[]
{
"~/{0}.aspx",
"~/{0}.ascx",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
};
MasterLocationFormats = new[]
{
"~/{0}.master",
"~/Shared/{0}.master",
"~/Views/{1}/{0}.master",
"~/Views/Shared/{0}.master",
};
PartialViewLocationFormats = ViewLocationFormats;
base.ViewLocationCache = new DefaultViewLocationCache(TimeSpan.FromMinutes(30));
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
string controller;
string areaPartialName;
ViewEngineResult result = null;
// Performance enhancement #1:
// Don't attempt to resolve absolute paths as area paths
// ========================================================
if (partialViewName.StartsWith("~"))
return base.FindPartialView(controllerContext, partialViewName, useCache);
if (controllerContext.RouteData.Values.ContainsKey("area"))
{
areaPartialName = FormatViewName(controllerContext, partialViewName, true);
result = base.FindPartialView(controllerContext, areaPartialName, useCache);
if (result != null && result.View != null)
return result;
areaPartialName = FormatSharedViewName(controllerContext, partialViewName, true);
result = base.FindPartialView(controllerContext, areaPartialName, useCache);
if (result != null && result.View != null)
return result;
}
// Performance enhancement #2:
// Resolve the view path internally, if possible. This avoids the
// slower method of view path resolution used by the ViewPathProviderViewEngine
// ========================================================
controller = controllerContext.RouteData.GetRequiredString("controller");
foreach (string fmt in base.ViewLocationFormats)
{
var path = string.Format(fmt, partialViewName, controller);
var path2 = controllerContext.HttpContext.Request.MapPath(path);
if (File.Exists(path2))
return base.FindPartialView(controllerContext, path, useCache);
}
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
string controller;
string areaViewName;
ViewEngineResult result = null;
// Performance enhancement #1:
// Don't attempt to resolve absolute paths as area paths
// ========================================================
if (viewName.StartsWith("~"))
return base.FindPartialView(controllerContext, viewName, useCache);
if (controllerContext.RouteData.Values.ContainsKey("area"))
{
areaViewName = FormatViewName(controllerContext, viewName, false);
result = base.FindView(controllerContext, areaViewName, masterName, useCache);
if (result != null && result.View != null)
return result;
areaViewName = FormatSharedViewName(controllerContext, viewName, false);
result = base.FindView(controllerContext, areaViewName, masterName, useCache);
if (result != null && result.View != null)
return result;
}
// Performance enhancement #2:
// Resolve the view path internally, if possible. This avoids the
// slower method of view path resolution used by the ViewPathProviderViewEngine
// ========================================================
controller = controllerContext.RouteData.GetRequiredString("controller");
foreach (string fmt in base.ViewLocationFormats)
{
var path = string.Format(fmt, viewName, controller);
var path2 = controllerContext.HttpContext.Request.MapPath(path);
if (File.Exists(path2))
return base.FindView(controllerContext, path, masterName, useCache);
}
return base.FindView(controllerContext, viewName, masterName, useCache);
}
private static string FormatViewName(ControllerContext controllerContext, string viewName, bool isPartial)
{
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string area = controllerContext.RouteData.Values["area"].ToString();
return "~/Areas/" + area + "/Views/" + controllerName + "/" + viewName + (isPartial ? ".ascx" : ".aspx");
}
private static string FormatSharedViewName(ControllerContext controllerContext, string viewName, bool isPartial)
{
string area = controllerContext.RouteData.Values["area"].ToString();
return "~/Areas/" + area + "/Views/Shared/" + viewName + (isPartial ? ".ascx" : ".aspx");
}
}
}
2 comments