Hosting the ASP.NET runtime in your own application


Applies to:
  • ASP.NET runtime
  • Windows Forms
Summary: It's quite easy to host the ASP.NET runtime in your own application. This first article in a series of two demonstrates how to execute ASP.NET pages and display the result in Internet Explorer.

Download the sample code for this article at the author's website.

Introduction

In this article I want to show you how easy it is to run ASP.NET pages inside your own application without needing IIS. For example, when a user is working on Windows XP Home Edition it's not possible to run IIS on the machine.

What I'll describe here is exactly the same as what 'Cassini' does. Cassini is a lightweight sample web server which is included in ASP.NET Web Matrix and demonstrates the hosting of ASP.NET pages. Users of ASP.NET Web Matrix can run and test their applications on their local computer by running Cassini. This is very exciting for users who can't install IIS on their computer but want to take their first steps in ASP.NET development.

In the second part of this article, I'll explain how you can extend and use the Cassini web server to create a cd-rom with an ASP.NET application on it, which can be launched without a setup program. The full source of the Cassini web server is available as a free download on the ASP.NET community site.


What do we want to do?

First of all, let's go over the functionality we want to create in our application. We want to run a simple ASP.NET page without needing a full blown web server like IIS on our machine. Since ASP.NET is processed 'server-side', we can't simply enter the local path to an aspx file in a browser like Internet Explorer. This will cause the source code of this file to be displayed or downloaded, and this is of course not the behaviour we are looking for. So, what we need to do is to 'execute' the ASP.NET page in our application, get the returned output and store it in a text file. Since we will only host aspx pages that produce HTML, we can display the file using Internet Explorer.
The solution actually consists of two parts:
  • Create a set of classes to host the ASP.NET runtime
  • Develop a 'client' application to call those classes and to display the result
I'll write two different, small client applications. The first one is a simple Windows Forms application which displays the result in an Internet Explorer control on a form. In the second one, I'll show you how to create a command-line tool to execute an aspx page and view the results in an instance of Internet Explorer.

Creating the host for the ASP.NET runtime

Let's kick off with the first part: creating a class library which can host the ASP.NET runtime to execute (in our case simple) .aspx pages. The .NET Framework provides a mechanism which allows us to use the ASP.NET runtime without requiring the ISAPI extension for IIS.

So, let's open up an instance of Visual Studio .NET 2003 and create a new type class library project in C# (the code for VB.NET is available as well):


First of all, set the properties for the AspNetHost project using the Solution Explorer like this:


Now, delete the Class1.cs file and add a new Class file called Host.cs. The first thing we need to do is to inherit this class from the MarshalByRef class. This is required because the ASP.NET runtime is using application domains to run web applications. By inheriting from the MarshalByRef class, we're enabling access to our object across application domain boundaries. If you've ever written a .NET Remoting application yourself, I'm sure you have used this class before.
public class Host : MarshalByRefObject

We won't use a constructor in our Host class (so, remove the constructor logic), but we'll provide a 'factory method', that's a static method, to create an instance of the Host object:
public static Host Create()

A little further I'll explain why I'm using the factory pattern and not a constructor patterns.

Now, it's really coding time. But don't expect to see many lines of code here, since the .NET Framework will do most of the work for us. In fact, our Create() method will consist of just one single line of code. But before we can write this piece of code, we'll need to import a namespace which contains the definition of the ApplicationHost class. First, add a reference to the System.Web.dll assembly:


On top of the host.cs file, import the System.Web.Hosting namespace where the ApplicationHost class is defined:
using System.Web.Hosting;

We'll also need to have access to the Directory class to find the current directory, so import the System.IO namespace as well:
using System.IO;

Now we can complete the Create() method as follows:
return (Host) ApplicationHost.CreateApplicationHost(
  typeof(Host), "/", Directory.GetCurrentDirectory());

This code makes clear why we're using a factory pattern as mentioned before. Since we need to call the ApplicationHost.CreateApplicationHost method to get our object, pass in the type of our own class (using typeof) and to cast it to our own class, we can't use a constructor. In fact, we're writing a factory method which invokes another factory method.

Let me explain this. The ApplicationHost represents an application domain for our ASP.NET application. If you've already used .NET remoting before, the concept of application domains will probably be clear. This class has only one (static) method, CreateApplicationHost, that creates and configures an application domain using 3 parameters: the type of the Host (which must be an object inheriting from the MarshalByRef class), the virtual directory we want to use and the physical directory of the application. Since we're writing a very simple application we'll pass through some simple values for the virtual directory (the default "/") and the physical path (that is the folder where the application was lauched). That's it.

Our class needs one more method: the Process method that will be used to process a request to the application. Let's take a look at the signature first:
public void Process(string page, string query, string output)

The first parameter will take the page we want to process; the second one will contain the query string parameters of the request. The last one is used to specify the path where the result needs to be stored. This method will be a little more complex. In fact it will be 4 times as complex as our first method, since it will have 4 lines. But before we can continue we need to import another namespace:
using System.Web;

Okay, let's write the implementation of the Process method:
TextWriter writer = File.CreateText(output);
SimpleWorkerRequest request = new SimpleWorkerRequest(page,query,writer);
HttpRuntime.ProcessRequest(request);
writer.Close();

The first line creates a new file on the specified location (parameter 3 of the method). Then we create a new instance of the type SimpleWorkerRequest. In the constructor we specify the page that is requested, the text of the query string and the System.IO.TextWriter object that will capture the output from the response. The Cassini web server also uses the SimpleWorkerRequest class in its Request class. This Request class inherits from the SimpleWorkerRequest class, but I'll elaborate on this in the second part of the article. The third line is the one where all the magic happens: the HttpRuntime, which provides many ASP.NET run-time services, is called to process the request. Finally, we close the TextWriter.

We're done for this project. The only thing left to do is to compile the project...

A simple Windows Forms client

It's time to see the results of our work in the class library. Add a new project to the solution: make a Windows Forms application in C# and name it WinHostDemo:


First of all, delete Form1.cs and add a new form with a nicer name, Browser.cs. Now change the property WindowState to Maximized. Add an entry point for the application in the code of Browser.cs:
[STAThread]
static void Main()
{
  Application.Run(new Browser());
}

When you've done so, set the WinHostDemo to be the start up project in the solution.

Since we want to have a browser in here, right-click in the Toolbox and choose Add/Remove Items and add the 'Microsoft Web Browser' COM component:


Finally, drag and drop a 'Microsoft Web Browser' control from the Toolbox to the Browser.cs form and rename it to 'internetExplorer'. To make our application look fine, change the Dock property of the control to 'Fill'. The result is displayed in the next image:


It's coding time again. Double-click on the title bar of the Browser.cs form to add an event handler for the Form_Load event. Before we can continue, we need to add a reference to our class library project in the Solution Explorer:


In the browser.cs file, import the Msdn.Demo namespace on top of the code, as well as System.IO and System.Configuration:
using Msdn.Demo;
using System.Configuration;
using System.IO;

Now we can actually start to code the Browser_Load event handler method:
string home = ConfigurationSettings.AppSettings["home"];
string temp = Path.GetTempPath();
string output = (temp.EndsWith("\\") ? temp : temp + "\\") + "~result.htm";

Host host = Host.Create();
host.Process(home,String.Empty,output);

object o = null;
internetExplorer.Navigate("file://" + output, ref o, ref o, ref o, ref o);

I'll explain briefly what this code does. The first line of code reads the page that should be displayed from the application's configuration file. We still need to make this file and will do this right away:
  • Add a new item to the project in the Solution Explorer.
  • In this dialog choose "Application configuration file". The file will be called app.config. When Visual Studio .NET compiles this project, it will copy this file to the output folder and automatically rename it to the application's configuration file (i.e. WinHostDemo.exe.config).
Now, modify the file as follows:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="home" value="default.aspx" />
  </appSettings>
</configuration>

This will cause the first line of code to work properly. In the second and third line, the temporary folder on the client is used to create the path to the temporary output file (called ~result.htm).

The two next lines are the most important ones:
Host host = Host.Create();
host.Process(home,String.Empty,output);

This creates an instance of our ASP.NET runtime host class and processes the request to the file specified in the app.config file. The output is written to the ~result.htm file in the temporary folder using the third parameter in the host.Process method.
The last two lines browse to the page using the Internet Explorer control on the form.
Okay, it's time to try to execute the solution with the magic keystroke F5. If everything works fine, you should see this exception:


Break the execution and take a look at the location where the exception was thrown:


The problem is that the ApplicationHost.CreateApplicationHost method cannot find the Host class. You can stop the debugger. If you would take a look at the Cassini source code, you would see that this is solved by registering this class in the GAC (Global Assembly Cache). This would be a possible solution and I'll explain shortly what you should do to get this to work:
Go to the Visual Studio .NET Command Line prompt and navigate to the project folder of the AspNetHost project. Use the sn.exe tool to create a new strong key for the assembly which we'll use to register the assembly in the GAC:
sn -k AspNetHost.snk



Go back to Visual Studio .NET and add the AspNetHost.snk file to the solution (use the Show all files button in the Solution Explorer and right-click the AspNetHost.snk file, Include in project). Now open the AssemblyInfo.cs file and modify it as follows:
[assembly: AssemblyKeyFile("..\\..\\AspNetHost.snk")] [assembly: AssemblyKeyName("AspNetHost")]

Compile the AspNetHost class library.
Go back to the Visual Studio .NET command line and go to the bin\debug folder and add the strong-named assembly to the GAC using this command:
gacutil -i AspNetHost.dll



Now, run the solution again using F5. As you can see the application now works fine and the result looks like this:


This error occurs because the default.aspx file couldn't be located. Finally, let's fix this and create a default.aspx page. I've created a simple page using the ASP.NET Web Matrix which uses a DataGrid to display the contents of the pubs database on my SQL Server. This page should be stored in the WinHostDemo\bin\Debug folder:


Note: If you want to run this sample, you should edit the DSN in the default.aspx file to point to your database server with the correct login and password.

When you run the application again you'll see this:


It will take a while before the page is compiled and executed by ASP.NET. Since our target is to create an application which will run from a CD-ROM to display a website which is stored on the same CD-ROM, I don't consider this a problem.

Solving the problems

There are still some problems we need to fix: we need to register an assembly in the GAC before we can launch the application. We currently can't process links and postbacks on the page. Since .aspx files and web controls will create links which will be called by the Internet Explorer browser control, this will cause problems.
Before we solve the first problem, let's take a look at the second one. I've made a new page named cal.aspx with this code:
<form runat="server"> <asp:Calendar id="cal" runat="server" /> </form>

In the app.config I've changed the home page to cal.aspx:
<add key="home" value="cal.aspx" />

When you run the application again everything looks fine:


But when you try to click on a data, a postback to the 'server' will be done. Remember: we only display an HTML file in our browser. So, there is no server! When we're requesting the cal.aspx page again, we'll get an error stating that the page cannot be displayed.
I'll fix this problem in the next part of the article, when I'll explain how to modify Cassini to run from a CD-ROM. The problem will be solved then because we'll launch a lightweight web server on the client's machine to host the ASP.NET runtime. Be sure to check out MSDN Belux for the next episode.
But there was still another problem to fix: we need to register the assembly of our self-written ASP.NET runtime host in the GAC. This is not what we want to do, since we do not want to install anything on the computer when running from the CD-ROM.
It took me a few hours to find out what the solution is for this problem. I'll begin to remove the assembly from the GAC. Go to the Visual Studio .NET Command Prompt again and execute this command:
gacutil /u AspNetHost



When you try to launch the application now, it won't work anymore and that's exactly what we want.

Apparently ApplicationHost's static CreateApplicationHost method looks in the GAC as well as in a subfolder called "bin" for the assembly of the host. In our case this is the assembly where the Host class is stored in. There is currently no way to change this behaviour of the CreateApplicationHost method, which might be a little annoying. But with this information we can solve the problem quite easily:
  • Open Windows Explorer and go to the bin\Debug subfolder in the WinHostDemo application solution folder.
  • Create a new folder "bin" in there.
  • Move the files AspNetHost.dll and AspNetHost.pdb to that subfolder.



That's it. Let's execute the application again, and it works. If you want to be excited, just go ahead. I was excited too when I finally got it to work.

The console ASP.NET client

I promised to write a second client application as well. In this last paragraph I'll develop a little command line tool which can be used to execute a simple aspx page and store the result in a file. When the application is ready, we will write a simple batch file to open up Internet Explorer as well to view that file.

In Visual Studio .NET, add a new 'C# Console Application' project to the solution and call it 'ConsoleDemo':


Rename the Class1.cs file to Tool.cs and change the name of the class in the code as well:
class Tool

Add a reference to the AspNetHost project in the Solution Explorer for the current project as we did before with the Windows Forms application. When you've done so, import the Msdn.Demo namespace in the Tool.cs:
using Msdn.Demo;

Now, add this piece of code to the Main method:
if (args.Length != 1 && args.Length != 2)
{
  string app = Environment.GetCommandLineArgs()[0];
Console.WriteLine("Usage: " + app + " in [out]");
  Console.WriteLine("in - source .aspx file");
  Console.WriteLine("out - (optional) output file");
  return;
}

With this piece of code, the application checks the number of parameters that were specified at the command line. As you can see, we'll allow the application to be used in two ways:
  • With one parameter, being the input aspx file. The output will be written to the screen.
  • With two parameters. The result of the input aspx file in the first parameter will be written to a new file specified in the second parameter.

To support the version with only one parameter we'll need to extend the Host class with one extra method. Go back to the Host.cs file in the AspNetHost project and add this overload for the Process method:
public string Process(string page, string query)
{
  TextWriter writer = new StringWriter();
  SimpleWorkerRequest request = new
SimpleWorkerRequest(page,query,writer);
  HttpRuntime.ProcessRequest(request);
  writer.Close();


  return writer.ToString();
}

Compile the AspNetHost project again.
Back in the ConsoleDemo project, add this piece of code at the bottom of the Main method:
Host host = Host.Create();
if (args.Length == 1)
  Console.Write(host.Process(args[0],String.Empty));
else
  host.Process(args[0],String.Empty,args[1]);

We're finished now. Compile the entire solution, it should complete without errors. There's one more thing we need to do. In Windows Explorer go to the bin\Debug subfolder of the ConsoleDemo project folder and create a folder bin. Copy the AspNetHost.dll and AspNetHost.pdb files to that folder.
It's time to take a look at the results. Start a command line and chdir to the bin\Debug folder of the ConsoleDemo project. Add a little default.aspx test page to the folder:
<form runat="server"> <asp:Calendar id="cal" runat="server" /> </form> </textcode>

Execute those commands:
ConsoleDemo.exe default.aspx

And:
ConsoleDemo.exe default.aspx default.htm

The output should look like this (for the first command):


Finally I'll wrap this command in a little batch file. Run Notepad.exe test.bat from the command-line to create a new test.bat file and add this piece of 'code':
@echo off
ConsoleDemo.exe %1 preview.htm
start preview.htm

Close Notepad and save the batch. At the command-line run
test default.aspx

It will open up Internet Explorer and will show the results of the processing of the default.aspx file:


Support for query string parameters

It would be nice to have support for query string parameters as well. In this last paragraph I'll extend the application to support another command-line parameter which can be used to specify a query string. In the tool.cs file modify the Main method as follows:
if (args.Length != 2 && args.Length != 3)
{
  string app = Environment.GetCommandLineArgs()[0];
  Console.WriteLine("Usage: " + app + " in query [out]");
  Console.WriteLine("in - source .aspx file");
  Console.WriteLine("query - the query string");
  Console.WriteLine("out - (optional) output file");
  return;
}

Host host = Host.Create();
if (args.Length == 2)
  Console.Write(host.Process(args[0],args[1]));
else
  host.Process(args[0],args[1],args[2]);

Compile the solution again and go back to the command prompt. Since the tool now supports query string parameters as well, we'll create a new aspx file that uses a query string. Create default2.aspx using Notepad (or Visual Studio .NET, if you want). Add the following piece of code:
<%@ Page Language="C#" %>
<% Response.Write(Request.QueryString["testA"]); %>
<% Response.Write(Request.QueryString["testB"]); %>

and save the file.
You can try to run our new version of the tool using this command:
consoledemo default2.aspx "testA=MSDN.BE&testB=%20Rocks!"

Of course you'll need to modify the test.bat file as well:
@echo off
ConsoleDemo.exe %1 %2 preview.htm
start preview.htm

Remark: the second parameter is needed to get this batch to work. If there's no query string an empty string can be passed through ("").

In conclusion

In this article I demonstrated how easy it is to host the ASP.NET runtime inside your own applications. My target is to create an application that can be stored on a CD-ROM to launch an ASP.NET web application from that CD-ROM. However, there's still one problem that needs to be solved: how can our application deal with postbacks? I'll cover this in the next part of the article in which I'll describe how the Cassini web server works and how to enable our application to run ASP.NET websites without a real web server like IIS.

About the author

Although being a student Informatics, Bart De Smet is already actively developing applications with the .NET Framework since the first beta. His interests are rather broad: going from C# and Visual Basic .NET development and the internet (ASP.NET, Web Services, .NET Remoting) over systems administration (Windows 2000, Windows XP and Windows Server 2003, Active Directory, Exchange Server 2003) to .NET Enterprise Servers (SQL Server, BizTalk). Bart is also responsible for the maintenance of the network of the "College" in Zottegem (Windows Server 2003) and is actively involved in the Belgian .NET community.

In 2001 he built the first site using .NET Passport in Belgium. Recently Bart became a Top 25 Poster and moderator of the official ASP.NET Forums. Bart is also a regular speaker about ASP.NET and Windows Server 2003 on AAL and he also takes care of several workshops.

You can contact him at info@bartdesmet.net or on his website http://www.bartdesmet.net.