Pre-Compiling ASP.NET Web Pages
Updated Recently -- See Notes and Credits following the Article
Overview
We all know ASP.NET compiles our web pages the first time they are hit,
which helps to make our sites faster than was possible using Classic ASP.
But a frequent complaint is that this first hit is significantly slower,
so many of us have wished for a way to pre-compile our site's web pages.
Even those of us that use VS.NET with its compiled code-behind need this,
since the aspx content pages are still not compiled until the first hit.
Current Solutions
I know of two current solutions: (1) batch files that hit each page and
(2) a single page with lots of IFrames hooked up to hit each page instead.
Both of these solutions do work, but they have two significant problems:
(1) manual setup of links and (2) starting the batch file or special page.
Even if you automate building the links and running script on deployment,
you still have problems if your site is restarted or any changes are made.
Better Solution
I have created a GlobalBase class that will automatically hit each page
whenever your site is started, either due to a first hit or a restart.
You can
download
the WilsonWebCompile.dll and simply set your Global.asax
file to inherit from the Wilson.WebCompile.GlobalBase class -- thats all.
Make sure you place the dll into your bin directory, and you will need to
also set a reference to it in your project file if you are using VS.NET.
Listing 1: Example Usage
C# CodeBehind -- Global.asax.cs:
public class Global : Wilson.WebCompile.GlobalBase
VB CodeBehind -- Global.asax.vb:
Public Class Global
Inherits Wilson.WebCompile.GlobalBase
No CodeBehind -- Global.asax:
<%@ Application Inherits="Wilson.WebCompile.GlobalBase" %>
C# Source Code
You don't need the
source code
if you just use the WilsonWebCompile.dll,
but there are some situations that may require modifications to the code.
The current GlobalBase class will loop through all of your sub-directories
and hit all the aspx files (also ascx, asmx, and ashx) that it finds there.
You may not want some sub-directories or files to be included in the loop,
or you may have other custom file extensions that you also want compiled.
Listing 2: GlobalBase.cs
using System;
using System.IO;
using System.Net;
using System.Web;
using System.Web.UI;
namespace Wilson.WebCompile
{
public class GlobalBase : System.Web.HttpApplication
{
private static bool needsCompile = true;
private static string applicationPath = "";
private static string physicalPath = "";
private static string applicationURL = "";
private static System.Threading.Thread thread = null;
// Override and Indicate Files to Skip with Semi-Colon Delimiter
protected virtual string SkipFiles {
get { return @""; }
}
// Override and Indicate Folders to Skip with Semi-Colon Delimiter
protected virtual string SkipFolders {
get { return @""; }
}
public override void Init() {
if (GlobalBase.needsCompile) {
GlobalBase.needsCompile = false;
applicationPath = HttpContext.Current.Request.ApplicationPath;
if (!applicationPath.EndsWith("/")) { applicationPath += "/"; }
string server = HttpContext.Current.Request.ServerVariables["SERVER_NAME"];
bool https = HttpContext.Current.Request.ServerVariables["HTTPS"] != "off";
applicationURL = (https ? "https://" : "http://") + server + applicationPath;
physicalPath = HttpContext.Current.Request.PhysicalApplicationPath;
thread = new System.Threading.Thread(new System.Threading.ThreadStart(CompileApp));
thread.Start();
}
}
private void CompileApp() {
CompileFolder(physicalPath);
}
private void CompileFolder(string Folder) {
foreach (string file in Directory.GetFiles(Folder, "*.as?x")) {
CompileFile(file);
}
foreach (string folder in Directory.GetDirectories(Folder)) {
bool skipFolder = false;
foreach (string item in this.SkipFolders.Split(';')) {
if (item != "" && folder.ToUpper().EndsWith(item.ToUpper())) {
skipFolder = true;
break;
}
}
if (!skipFolder) {
CompileFolder(folder);
}
}
}
private void CompileFile(string File) {
bool skipFile = false;
foreach (string item in this.SkipFiles.Split(';')) {
if (item != "" && File.ToUpper().EndsWith(item.ToUpper())) {
skipFile = true;
break;
}
}
if (!skipFile) {
string path = File.Remove(0, physicalPath.Length);
if (File.ToLower().EndsWith(".ascx")) {
string virtualPath = applicationPath + path.Replace(@"\", "/");
Page controlLoader = new Page();
try {
controlLoader.LoadControl(virtualPath);
}
finally {
System.Diagnostics.Debug.WriteLine(virtualPath, "Control");
}
}
else if (!File.ToLower().EndsWith(".asax")) {
string url = applicationURL + path.Replace(@"\", "/");
using (HttpWebRequest.Create(url).GetResponse()) {}
System.Diagnostics.Debug.WriteLine(url, "Page");
}
}
}
}
}
Conclusion
Using the provided WilsonWebCompile.dll, or modifying the GlobalBase.cs,
will finally allow you to always have your ASP.NET automatically compiled
with the very first page that is hit, instead of having to hit them all.
I've tested this on several sites so far, with different configurations,
but I would like to hear from you if your site required any modifications
so I can update this article and code with further scenarios for others.
Updates and Credits
I've been amazed how many people have been excited about this possibility,
even though its definitely more of a "hack" than a normal "real" solution.
I have now updated the code to "hit" user controls, web services, and more,
although it still fails with secured sites and should be tested carefully.
The async request has been replaced with synchronous on a separate thread
to avoid the problems async caused when it used up the available threads.
There are also properties you can override in your own Global.asax class
to specify any files and/or folders that should be skipped in this process.
Finally, see this
follow-up article for a similar "hack" that will try to
keep your web application always up and running, making it much speedier.
The code in this article has had a few changes since the original release,
mostly due to the questions, insights, and contributions from the following:
Christian Weyer
sent me the included HttpModule alternative implementation,
and was responsible for the ".as?x" filter to handle user controls and more.
David Miles
converted the original C# to VB, which is included and updated.
Joel Gray
discovered initial odd behavior with asynchronous web requests,
Ambrose Little
pointed out hitting pages does not work with secured sites, and
Ryan Trudelle-Schwarz
provided some feedback and code for user controls.
Ambrose also provided some additional user control code while helping me to
conclude that another "better" solution was actually not working very well.
Author Bio
Paul Wilson is a software architect in Atlanta, currently with a medical device company.
He specializes in Microsoft technologies, including .NET, C#, ASP, SQL, COM+, and VB.
His
WilsonWebForm
Control allows Multiple Forms and Non-PostBack Forms in ASP.NET.
He is a Microsoft MVP in ASP.NET and is also recognized as an ASPFriend's
ASPAce/ASPElite.
He is a moderator on Microsoft's
ASP.NET Forums, as well as one of the top posters.
He is certified in .NET (MCAD), as well as also holding the MCSD, MCDBA, and MCSE.
Please visit his website,
www.WilsonDotNet.com,
or email him at
Paul@WilsonDotNet.com.