Cutting Edge
ASP. NET Client-side Hosting with Cassini
Dino Esposito
Code download available at:CuttingEdge0301.exe(107 KB)
Contents
Hosting the ASP.NET Runtime
The ApplicationHost Class
A Sample ASP.NET Host
The Cassini Personal Web Server
Your Web Site on a CD
Conclusion
In the September and October 2000 issues of MSDN® Magazine I discussed how to build a client-side environment for ASP applications; that is, a serverless environment to run ASP pages (see Cutting Edge: A Client-side Environment for ASP Pages and Cutting Edge: A Client-side Environment for ASP Pages—Part 2). Those columns were inspired by the following scenario.
Suppose that one of your clients needs to distribute some online content using a CD—an encyclopedia, yellow pages, or a collection of documents, for example. The client needs a viewer application to be included with the CD and a flexible software architecture to deliver the contents. Furthermore, the client would prefer that the CD didn't inconvenience the user with special system requirements other than a minimum processor and a recent version of Windows®. This means that the final application should not rely on Microsoft® Internet Information Services (IIS) or the Personal Web Server included in home versions of Windows. It should run offline in a pure, serverless environment.
In many cases, the client has an existing Web site for online content. In other cases, she plans to set up a Web site for online content as part of the project. In this context, using ASP or ASP.NET to do the job would be an excellent choice because they are flexible and powerful enough to let you build the viewer application quickly and effectively. But is it really possible to host ASP or ASP.NET in a custom application?
When I first approached the problem about three years ago, ASP.NET had yet to be announced; there were only rumors about something really cool, tentatively called ASP+, which was due to be released shortly. At that time, no tool was available to render ASP pages in an offline manner outside of IIS. Worse yet, ASP was not designed to make such hosting easy. Thus I came up with an ASP emulator made of two components: a specialized browser and an ASP server module. The browser was built to intercept any form submission and URL requests and redirect them to my own ASP server module. The ASP server, in turn, would load the resource from disk and parse its contents to generate HTML code dynamically. The browser was also responsible for instantiating and initializing pseudointrinsic objects with a programming interface nearly identical to that of the various Response, Request, and Server ASP objects. Figure 1 summarizes the overall architecture.
Figure 1** Offline Server **
Although not ideal as a solution, it met my client's expectations and was integrated into a larger product that today still allows many professionals to consume content both online and offline. Judging from the feedback I had once the columns were published, it seems many developers faced similar challenges.
A few months later, Microsoft released the first beta of ASP.NET and I thought about revisiting my solution using the new product. ASP.NET is designed in a very modular way and, as such, lends itself quite well to being hosted in external applications, including IIS itself. However, the ability to host ASP.NET in a custom application doesn't equal an off-the-shelf solution for building an offline Web browser to ship on a CD. Hosting the ASP.NET runtime engine in a custom application is only the first step in the delivery of dynamic content offline. If you take a look at the architecture shown in Figure 1, you'll see that it's based on two distinct components—a request processor that also takes the user's input and an ASP source processor that produces the actual HTML code. Hosting the ASP.NET engine would simply replace the ASP server module from Figure 1. In reality, you need more—ideally, a browser and an embedded Web server.
The ASP.NET Cassini sample Web server (see https://www.asp.net/Projects/Cassini/Download) is a compact, local Web server that you can integrate and deploy with your solutions. Cassini employs the ASP.NET hosting API (the System.Web.Hosting namespace) to create a simple managed Web server. Socket connections are handled through the System.Net API. Cassini is available from Microsoft with source code. Figure 2 shows the typical architecture of an offline Web application based on Cassini. As you can see, the overall scheme is just like a typical Internet-based Web application, but much simpler. Obviously, Cassini is neither a full replacement for IIS nor the Microsoft version of an open source Web server. Cassini is a local Web server for handling local calls to local folders. I'll review the Cassini components and then conclude by showing you how to deploy a Web site on a CD.
Figure 2** Cassini Web App **
Hosting the ASP.NET Runtime
An ASP.NET application doesn't require IIS as the host module. In fact, ASP.NET doesn't even require a Web server to run. It exposes a well-known interface that any caller can use to connect and to ask the internal HTTP pipeline to process a request.
Two classes play a key role in the hosting of the ASP.NET engine—HttpRuntime and ApplicationHost. The former is the entry point in the pipeline of objects that, much like an assemblage chain, transforms the original HTTP request for an .aspx resource into fresh HTML text. The latter makes it possible for client applications to host the ASP.NET engine. The ApplicationHost class is responsible for creating the AppDomain in the host process in which all incoming requests for the new application will be handled.
Tim Ewald and Keith Brown provided good coverage of the HttpRuntime class internals in their article "HTTP Pipelines: Securely Implement Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET" in the September 2002 issue of MSDN Magazine. The HttpRuntime class gets involved only when the application host has received and preprocessed the request. The application host packs all the information about the request in a request class derived from the HttpWorkerRequest abstract class or, more likely, from its standard implementation class named SimpleWorkerRequest. After an instance of the request class is ready to use, the host hands the processing over to HttpRuntime, calling its ProcessRequest static method, as shown in the following code:
SimpleWorkerRequest req; req = new SimpleWorkerRequest(aspx, null, Console.Out); HttpRuntime.ProcessRequest(req);
The previous snippet shows the core code that fires the processing of an ASP.NET page. The code's execution is governed by a particular method on the host class created through ApplicationHost. I'll return to this topic later. For now, suffice it to say that the constructor of SimpleWorkerRequest takes the virtual path of the ASPX resource to process, an optional query string, and a text writer object to use for the output. Instead of the standard output console, you could use a stream writer object to save the HTML code to disk.
The ApplicationHost Class
The interaction between the host and the ASP.NET HTTP runtime is ruled by a particular Microsoft .NET Framework class called ApplicationHost. From the perspective of the ASP.NET HTTP runtime, the host is simply the caller—namely, the module that has created the current AppDomain and that is invoking the ProcessRequest method to serve a particular request. The interface between the ASP.NET HTTP runtime and the host is all in the operations accomplished by the ApplicationHost class. The HTTP runtime is oblivious to the characteristics of the caller—be it a full Web server like IIS, a local Web server like Cassini, or even a simple application like the one I'll build in a moment. ASP.NET serves any module that can call HttpRuntime.ProcessRequest and pass correct information. Figure 3 describes the relationship between the ASP.NET HTTP runtime and the rest of the world.
Figure 3** ASP.NET versus the World **
The first step for hosting ASP.NET in an application is creating a new application host. This is accomplished by calling the CreateApplicationHost static method on the ApplicationHost class. CreateApplicationHost creates a new AppDomain in the caller's process. A new AppDomain is needed because ASP.NET relies on settings that can only be set at the AppDomain level, and some only before AppDomain is created. Some of these settings are the app base path and the directory for the shadow-copy cache location. CreateApplicationHost works on a per-virtual-folder basis, meaning that a new host, and subsequently a new AppDomain, are created whenever a new virtual folder is accessed for the first time. (Note that a simple Web server like Cassini works with only one virtual folder at a time, but this is just a special case.)
After the host interface object is created, a typical host application starts listening to requests. If the host works like a Web server, it starts listening to incoming messages through port 80; otherwise it's whatever port you indicate. A request is then packed into a request class and handed over to the ASP.NET runtime.
The HttpRuntime.ProcessRequest method routes the Request object through a pipeline of objects. At the end of the pipeline, a new object comes out—an instance of a dynamically created class that inherits from the Page class and represents the .aspx page being requested. To top off the request, the HTTP runtime calls the ProcessRequest method on the Page class. The page's ProcessRequest method performs a number of tasks on the Page object, each of which is signaled with an event. The page is first initialized by creating an instance of a server control for each markup element with the runat=server attribute set. Next, the ASP.NET code loads the view state of the page and merges it with any post data (such as values of textboxes and checkboxes). Finally, the runtime executes any server code the client may have requested (mostly postback events), saves back the view state, and writes the HTML to the output writer.
The CreateApplicationHost static method is the only member of the ApplicationHost class. Its C# prototype is shown here:
public static object CreateApplicationHost( Type hostType, string virtualDir, string physicalDir );
The virtualDir argument shown in the previous snippet represents the virtual directory for the application domain being created, while the physicalDir argument indicates the file system path behind that virtual path—the disk folder from which requested .aspx files have to be loaded for that Web application. These pieces of information are associated with the domain and consumed by the ASP.NET factory objects to create HttpApplication objects (global.asax) and page objects (.aspx).
The first argument of CreateApplicationHost is a type object that evaluates to the type of the application-defined host class. This method returns an instance of the user-supplied class used to bridge the default AppDomain of the host program and the newly created AppDomain (see Figure 4). The host type object is a sort of proxy between the core code of the server-like application and the ASP.NET HTTP runtime living in the target AppDomain that CreateApplicationHost had previously created. The host executable might need to configure, start, and stop a particular Web application and subsequently need an ad hoc counterpart in the AppDomain. The host type, in turn, includes methods to listen to ports for HTTP requests. When a request arrives, a SimpleWorkerRequest object is created and passed to HttpRuntime.
Figure 4** Default AppDomain **
The request object is created in the same AppDomain that will actually service the request. HttpWorkerRequest is the base abstract class that defines all the properties and methods used by ASP.NET to process requests. SimpleWorkerRequest is a lightweight implementation of the base class and provides the ASP.NET runtime with minimal information such as the requested URL and the query string. It also receives the ASP.NET output into a stream writer object. You can extend SimpleWorkerRequest and override the appropriate HttpWorkerRequest methods if you think you need more preprocessing functionality for things like parsing headers and posted data. (Look at the Cassini source code for a concrete example of a class that extends SimpleWorkerRequest.)
A Sample ASP.NET Host
So much for the underpinnings of the ASP.NET hosting. Let's write some code to demonstrate how to host ASP.NET in a Windows Forms application. The idea is to use ASP.NET to generate help pages for the various features of the program. The user of the sample application types in a keyword and is promptly displayed an HTML page. The HTML page is created on the fly by the ASP.NET page that corresponds to the help required.
Figure 5 shows the core code of the Windows Forms application. Upon loading, the form initializes the application host. The root virtual folder is called /dino; its files are located in the Help subdirectory of the current folder. Note that /dino is just a name, so there's no need to create a file system directory or an IIS virtual folder for it. The physical path must exist, otherwise a "file not found" exception will be thrown.
Figure 5 InitializeApplication Host
private const string TEMPFILE = "__page__.htm"; private MyAppHost m_host; private void Form1_Load(object sender, System.EventArgs e) { // Create the application host InitHost(); } private void InitHost() { m_host = (MyAppHost) ApplicationHost.CreateApplicationHost( typeof(MyAppHost), // class to interface "/dino", // virtual (root) directory Directory.GetCurrentDirectory() + "\\help"); // find ASPX here } private void theSearch_Click(object sender, System.EventArgs e) { string helpFile = ""; string keyword = theKeyword.Text; switch(keyword) { case "ASP.NET": helpFile = "aspnet.aspx"; break; case "ADO.NET": helpFile = "adonet.aspx"; break; default: helpFile = "std.aspx"; break; } // Create a temp HTML file in the current directory string url = PrepareHelpPage(file); // Refresh the UI object o1=null, o2=null, o3=null, o4=null; doc.Navigate(url, ref o1, ref o2, ref o3, ref o4); } private string PrepareHelpPage(string file) { string query = "keyword=" + theKeyword.Text; m_host.CreateHtmlPage(file, query, TEMPFILE); return "file://" + Directory.GetCurrentDirectory() + "/" + TEMPFILE; }
The host type is a class called MyAppHost whose source code is shown in Figure 6. MyHostClass has only one method, CreateHtmlPage. The number and the prototype of the host class members are completely up to you. What really matters is that you devise a programming interface that reflects what you are going to do. In this case, the ultimate goal is fairly simple—just take the ASPX file and translate it to HTML. A SimpleWorkerRequest object fits the bill and the prototype of its constructor is exactly what's needed.
Figure 6 MyAppHost
using System; using System.Web; using System.Web.Hosting; using System.IO; namespace Samples { public class MyAppHost : MarshalByRefObject { public void CreateHtmlPage(string aspx, string query, string html) { StreamWriter stmw = new StreamWriter(html); SimpleWorkerRequest swr = new SimpleWorkerRequest( aspx, // ASPX file name query, // Query string without the ? char (i.e. // p1=hello&p2=world) stmw); // Output stream (i.e., Console.Out) HttpRuntime.ProcessRequest(swr); stmw.Close(); } } }
The first argument is the name of the ASPX file to process. The second argument is an optional query string. Note that if you use a URL with command-line arguments, it will be mistaken for the real name of the file. (IIS separates the URL from the query string while preprocessing the request.) The SimpleWorkerRequest class expects to receive URL and query as distinct entities. Furthermore, note that the initial ? character of the query string must be removed.
Finally, the third argument of the constructor is a stream writer object that will be used to buffer the output text. Everything that the ASPX page will send out through Response (and higher-level methods such as Page.Render) is accumulated in this writer. I make the writer work on top of a temporary HTML file, so when I close the writer the HTML file is created. Finally, the embedded WebBrowser ActiveX® control displays the page.
There is no limit to the ASP.NET features you can use—the ASP.NET HTTP runtime is the driving force. Feel free to use configuration files, ADO.NET adapters, view state, output caching, XML, HTTP modules, and whatever else involves ASP.NET and the .NET Framework. In addition, the application works irrespective of the status of IIS. You can keep IIS running or even stop it—the ASP.NET runtime hosted by the sample application won't be affected. (IIS is just another application host, albeit a particularly rich and complex one.)
Since ASP.NET is under the hood, the rules for which assemblies are needed are the same as in ASP.NET. This means that the assembly that contains the host class must be copied both in the ASP.NET host application's directory and in the Bin subdirectory of the ASP.NET application folder. Another option is to put that assembly in Global Assembly Cache (GAC) as Cassini does. The reason is that two modules need to load the host class: the host application that is mimicking IIS and the invoked ASP.NET worker process. Both executables expect to find the host class in their own paths for assemblies. For the host application the path is the current folder; for the ASP.NET worker process the path is the Bin subdirectory of the folder that contains the .aspx pages or the GAC. Just because the host class must be available in two distinct places, it's strongly recommended that you code it in a separate assembly. However, embedding application and host classes in the same executable is not illegal and works just fine. For the sample application, for instance, you must have a copy of MyAppHost in the same folder as the executable (the Folder\Bin\Release directory) and a second copy in the Folder\Bin\Release\Help\Bin folder (Bin is below the ASPX files, as a regular ASP.NET application would have required).
The Cassini Personal Web Server
The ability to host ASP.NET in a user application is perhaps less exciting than it sounds. Sure, you can build smart help systems and devise offline tools to precompile the pages of a Web site to HTML. But can ASP.NET hosting alone let you build serverless Web-like applications that run on a local machine without IIS?
If you're not convinced yet, think about what happens if the ASP.NET page contains interactive controls and posts back. Let's review this case in the context of the sample host application. An HTML page is displayed through a WebBrowser control—an instance of the browsing engine of Microsoft Internet Explorer. The browser always processes a click on a link by forwarding an HTTP POST command through port 80. Since the application host has no listener active on port 80, a "page not found" error is returned. You need to build a custom layer on top of the client to intercept and redirect the postback to the Request object specific to the application host. In other words, you can realistically exploit ASP.NET hosting only to render static, read-only pages. Incidentally, this aspect also proved to be the highest hurdle to overcome in the September 2000 column I mentioned earlier. While it's fundamental piece of the puzzle, ASP.NET hosting alone is not sufficient to build a real-world serverless environment to consume Web sites offline. It needs to be accompanied by a mini Web server that understands what the browser wants. The Cassini sample Web Server is the ideal tool for this task.
Figure 7** Starting Cassini **
The Cassini Web server is a Windows Forms application called cassiniwebserver.exe. You start the Web server by specifying the port number to listen to, the virtual directory to monitor, and the physical path behind that. You start up Cassini through the form shown in Figure 7. Note that Cassini can process only one virtual folder at a time and only accepts requests made through localhost. This is exactly what you need to deploy existing Web sites on offline media such as a CD or DVD. Cassini is a managed application whose executables take no more than 70KB. In addition to the program, cassiniwebserver.exe, you also need to take into account the application host DLL—cassini.dll. The application host assembly must be stored in the GAC using the following command-line instruction:
gacutil /i Cassini.dll
Figure 8 lists all the public classes that make up the application host for the Cassini Web server. All classes are compiled in the cassini.dll assembly. The Server class is the entry point in the host executable. Its constructor creates the application host indicating the Host class as the host type.
void CreateHost() { m_host = (Host) ApplicationHost.CreateApplicationHost( typeof(Host), vdirPath, filesPath); m_host.Configure(this, port, vdirPath, filesPath,_aspnetPath); }
Figure 8 Cassini Classes
Class Name | Description |
---|---|
Server | Provides the public API to start and stop the Web server and creates the application host. Caches configuration information such as port number and virtual directory. |
Host | Remotable object (inherits from MarshalByRefObject) represents the Cassini's bridge-head in the child AppDomain that hosts ASP.NET. The class is in charge of opening a socket on the port and accepts HTTP requests when they arrive. |
Connection | An instance of this class is created to process the request. It is passed the opened socket and asked to create and activate a new Request object to actually carry out the request. The Request object receives a reference to the Connection object and uses it to write response data to the socket. |
Request | Inherits from SimpleWorkerRequest and implements the logic necessary to process the HTTP request. Parses the HTTP payload, processes headers, prepares the response buffer, and finally hands the request over to the ASP.NET HTTP pipeline calling the HttpRuntime.ProcessRequest method. While processing the request and generating the response, ASP.NET calls back some of the methods of the Request class. The Request's implementation of these methods funnels the calls into the parent Connection object to access the socket. |
Instances of the Server class are created in Cassini's AppDomain, whereas the Host object belongs to the AppDomain that was just created to process HTTP requests. The Server object also provides a sort of public API to start, stop, and configure the Web server. The Web server (cassiniwebserver.exe) sees this object as its unique point of contact with the ASP.NET back end.
The Host class represents a remotable object and inherits from MarshalByRefObject. A remotable object is an object that can be called from within a different AppDomain. The Host class is responsible for opening a socket on the specified port and listening to incoming packets. When a request arrives and is accepted, the class creates a new Connection object to process the request:
Connection conn = new Connection(this, (Socket) acceptedSocket); conn.ProcessOneRequest();
The Connection class receives the socket and starts working on the HTTP packets. The Connection object is primarily an intermediary between the host and the actual worker request. It first creates the Request object and then asks it to process the payload:
Request req = new Request(host, this); req.Process();
The Request object derives from SimpleWorkerRequest. It examines the HTTP packet, extracts and preprocesses the message headers, and prepares the response buffer. When it's finished, it simply hands the request out to the ASP.NET HTTP pipeline for the actual generation of the corresponding HTML response text. The entry point in the ASP.NET pipeline is the ProcessRequest method of the HttpRuntime object. While carrying out the request, ASP.NET calls back several methods on the Request object. In particular, all the data sent out through the Response object results in a call being made to one of the public methods of the host's worker Request. The actual implementation of Cassini uses the Request class to pass this response data to the methods of the Connection object which, in turn, will route the data through the open socket and the port. For this reason, an instance of the Connection object is passed to the constructor of the Request class.
If you think about Cassini's architecture and compare it to Figure 4, you can't help but recognize a common pattern. Cassini is the right tool for building serverless ASP.NET applications because it combines two key features: the ability to host ASP.NET to process ASPX requests and a simple, local Web server infrastructure. Using Cassini, you don't need custom code running on top of the viewer in order to process all page postbacks. Cassini is the missing link that now makes it possible to deliver entire Web sites on a CD without IIS.
Your Web Site on a CD
When I first examined Cassini, I took the following steps to test its effectiveness. First, I stopped IIS and launched the Cassini executable, as shown in Figure 7. Note that in order to test Cassini you must first stop IIS, otherwise a port conflict will occur. Furthermore, Cassini only processes calls that go through https://localhost; you can't access the local pages using the machine name (like https://msdnmag) nor can you access Web pages on a Cassini-equipped machine from a remote machine.
After launching Cassini and stopping IIS, I opened Internet Explorer and typed https://localhost. To my surprise, I was running my ASP.NET-based localnet without IIS! It goes without saying that if pages contain links to Internet Web sites, they will work as usual as long as an Internet connection is available.
However, to successfully pack a Web site onto a CD, a few preliminary checks must be performed. For one thing, make sure that all the links in the pages are either relative or rooted explicitly to localhost. Otherwise, you'll get an HTTP 403 error (access forbidden). To deploy the Web site offline, copy all the files to the storage medium along with the Cassini executables and the .NET Framework redistributable. Configure the setup to copy the Web site tree and install the cassini.dll to the client machine's GAC. In addition, you should start the Cassini Web server. You should also have the setup check for IIS and stop it if it's running.
If you're using Cassini for your offline Web application, you are better off disabling IIS altogether. By the way, this is exactly what Web Matrix does when you try to build an ASP.NET page. Web Matrix is a community-supported, free IDE specifically designed for ASP.NET applications (download it from Microsoft at https://www.microsoft.com/web/webmatrix/). Web Matrix ships Cassini, but allows you to work with IIS if you can provide an IIS virtual folder. If you choose to use Cassini as the Web server, then Web Matrix warns that it is going to stop IIS. From this point of view, Web Matrix behaves just like your ASP.NET host should. Figure 9 shows the code you can use in the setup to stop IIS, start Cassini, and open the root of the local host.
Figure 9 Stop IIS, Start Cassini
using System; using System.Diagnostics; public class OfflineWebSite { public static void Main() { OfflineWebSite o = new OfflineWebSite(); return; } public OfflineWebSite() { StartCassini(); Console.WriteLine("Done."); } private void StartCassini() { // stop IIS Console.WriteLine("Stopping IIS..."); Process p = new Process(); p.StartInfo.FileName = "net.exe"; p.StartInfo.Arguments = "stop iisadmin /Y"; p.Start(); p.WaitForExit(); Console.WriteLine("IIS stopped."); // start Cassini Console.WriteLine("Starting Cassini..."); p.StartInfo.FileName = "cassiniwebserver.exe"; p.StartInfo.Arguments = "c:\\inetpub\\wwwroot\\intranet 80 /"; p.StartInfo.WindowStyle = ProcessWindowStyle.Minimized; p.Start(); Console.WriteLine("Cassini started."); // open the the localhost Console.WriteLine("Opening the local host..."); Process.Start("https://localhost"); } }
Conclusion
The System.Web.Hosting namespace has been available since the first beta of the .NET Framework, but it took a while to obtain full documentation for it. The fact that ASP.NET can be hosted by any managed executable is a great advancement. The ASP.NET runtime can also be hosted by unmanaged applications as long as they can host the CLR. This is exactly what happens with the ASP.NET worker process that IIS utilizes to serve ASPX requests—aspnet_wp.exe is a Win32® process, yet it hosts the common language runtime and subsequently ASP.NET. Cassini, on the other hand, is a determinant tool to allow ASP.NET applications to run offline in serverless scenarios.
Send your questions and comments for Dino to cutting@microsoft.com.
Dino Espositois an instructor and consultant based in Rome, Italy. He is author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET, both from Microsoft Press. Dino spends most of his time teaching classes on ASP.NET, ADO.NET, and XML. Get in touch with Dino at dinoe@wintellect.com.