Pages

Friday, September 02, 2005

Executing ASPX pages without a web server : Kaushal Patel

For the last couple of days, I?ve been writing about creating a standalone ASPX execution environment?a Windows Forms application that executes an ASP.NET page and displays the results in a Web Browser control.

Because of the way the ASP libraries are written, this effectively requires either a couple of communicating but distinct assemblies or a strongly-named assembly in the Global Assembly Cache. In my project, I wrote two distinct assemblies, which worked. But I wondered if I could overcome this requirement, with the hope that I would learn a thing or two in the process. It took some code spelunking (and a venture beyond what is documented), but I succeeded on both fronts.

The article that got me started on this project was Ted Neward?s "Hosting ASP.NET: Running an All-Managed HTTP Server," which I can?t recommend highly enough. Neward opted for the two assembly approach (or more exactly, the same assembly copied into two different directories). My code still retains a class from his example. But while he uses an ASP.NET function to spin up the app domain that the ASPX page runs in, I create mine from scratch.

Here?s a little console app I created:

using System;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Runtime.Remoting;
using System.Globalization;
namespace AspHostTest
{
public class MyExeHost : MarshalByRefObject
{
public void ProcessRequest(String page)
{
HttpWorkerRequest hwr =
new SimpleWorkerRequest(page, null, Console.Out);
HttpRuntime.ProcessRequest(hwr);
}
}
class MyAspHost
{
public static object CreateApplicationHost(Type hostType, string virtualDir, string physicalDir)
{
if (!(physicalDir.EndsWith("\\")))
physicalDir = physicalDir + "\\";
string aspDir = HttpRuntime.AspInstallDirectory;
string domainId = DateTime.Now.ToString(DateTimeFormatInfo.InvariantInfo).GetHashCode().ToString("x");
string appName = (virtualDir + physicalDir).GetHashCode().ToString("x");
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = appName;
setup.ConfigurationFile = "web.config"; // not necessary execept for debugging
AppDomain ad = AppDomain.CreateDomain(domainId, null, setup);
ad.SetData(".appDomain", "*");
ad.SetData(".appPath", physicalDir);
ad.SetData(".appVPath", virtualDir);
ad.SetData(".domainId", domainId);
ad.SetData(".hostingVirtualPath", virtualDir);
ad.SetData(".hostingInstallDir", aspDir);
ObjectHandle oh = ad.CreateInstance(hostType.Module.Assembly.FullName, hostType.FullName);
return oh.Unwrap();
}

static void Main(string[] args)
{
MyExeHost myHost = (MyExeHost)CreateApplicationHost(typeof(MyExeHost),"/",Directory.GetCurrentDirectory());
myHost.ProcessRequest("app.aspx");
}
}
}

To compile it, open a Visual Studio .NET command prompt (or any other command prompt session with the C# compiler in the path) and run this command:

csc /t:exe /r:System.Web.dll AppHostTest.cs
Put the exe file in a directory with an ASPX file named app.aspx. Here?s the simple file I?ve used in my tests:

Hello, world.


Today is
<%= DateTime.Now %>.



When you run AppHostTest.exe, you?ll see the raw html that the executed ASPX file creates.

In addition to running this program, you can use VS.NET to debug right into the ASP.NET page. For this to work, you?ll have to instruct the ASP.NET classes to compile your code with debug symbols. You can do this by creating a file called web.config in the same directory. All this file needs to contain is the following XML:







To load the debugger, run this command from the same directory:

devenv /debugexe AspHostText.exe
Load the ASPX file with the Open/File? menu choice, set a breakpoint on a line of code, press F5, and you should see the debugger stop at that line.

Okay. That was fun, but what exactly is going on in the code?

What we?ve done is to create two classes. MyAspHost includes the Main method that runs the show and a static method I?ve called CreateApplicationHost. This latter method serves the same purpose (and takes the same parameters) as a static method of the same name in the System.Web.Hosting.ApplicationHost class. The ASP.NET method does more, and calls a lot of protected functions in the process.

My method does the minimum that is required to get a simple ASPX page to execute, which is to create an app domain, create an object in the app domain, and return a reference to this object to the calling program (in the default app domain). That's essentially what the ASP.NET classes do, but I've left out what isn't absolutely necessary. (The domain ID and application name could be given simpler names, but I chose to use the same algorithm that ApplicationHost.CreateApplicationHost uses.)

The other class in the file is the one that gets instantiated in the new app domain. All it does is create a System.Web.Hosting.SimpleWorkerRequest specifying the page to run and the output System.IO.TextWriter, then hand this request to a static method in the System.Web.HttpRuntime class, which does all the rest.

Next week I?ll write about the documented way to do this same thing, but I should mention the difference between this code and the Kosher code. The salient difference (apart from all the other things I left out) is that the ASP.NET code sets up a PrivateBinPath for the new app domain so it looks for the assembly it creates in a bin directory below the virtual root is uses. This explains the need for the second assembly?ASP.NET forces the Fusion assembly resolver to look for this assembly in a separate place. That?s what allows my code to be so simple; I allow Fusion to look for the new assemblies in the same directory as the executable.