giovedì 17 gennaio 2008

Generating ASP.NET web pages, without web servers

Today I had to find a way to embed some dynamic web pages in a Windows Forms application - better: I have the usual couple of .aspx and .cs files, and want to get the html output.
Some alternatives come up to mind: Cassini and Mono's XSP can easily be embedded in a standalone exe and provide a fully featured web server. However my requirements were much simpler, I don't need the ability to process requests and give responses - just spit out some HTML to display to the user, like a report. Moreover these approaches require listening to ports, running real web requests... I know that .NET contains all the required magic to process ASP.NET pages, so why can't I call some magic function and get the job done?

Well, it turns out that it's not as simple as calling a function, but with a few lines of code it can be done.

Create a solution with two projects. One is a Class Library project (called HostMarshaller), the other is our main app - for simplicity, let's choose a Console Application. In both projects add a reference to System.Web, and in the Console App add a reference to HostMarshaller.
Open the properties window of the Console App and switch to the Build Events tab. You'll need to add the following post-build command:

copy "$(TargetDir)HostMarshaller.dll" "$(TargetDir)bin\HostMarshaller.dll"

Now let's get to the code.
In HostMarshaller's generated Class1.cs, paste these lines:

using System;
using System.IO;
using System.Web;
using System.Web.Hosting;

public class HostMarshaller: MarshalByRefObject
public string Run(string page, string query)
StringWriter outputWriter = new StringWriter();
SimpleWorkerRequest request = new SimpleWorkerRequest(page, query, outputWriter);

return outputWriter.GetStringBuilder().ToString();

Now open the Program.cs in the Console App. In the main() body, paste these lines:
// For this to work, the HostMarshaller assembly must be located 
// in a \bin\ subfolder of the directory containing this EXE.
// A post-build command takes care of this.

System.Reflection.Assembly a = System.Reflection.Assembly.GetEntryAssembly();
string baseFolder = System.IO.Path.GetDirectoryName(a.Location);

HostMarshaller result = (HostMarshaller)ApplicationHost.CreateApplicationHost(typeof(HostMarshaller), "/", baseFolder);
string output = result.Run("report.aspx", String.Empty);


And add the using statement for System.Web.Hosting.
This code is enough to achieve our purpose. Now we just need to create the report.aspx.
Compile the project and ignore any errors, these are normal.

Open a file manager and go to the bin\Debug\ folder of the Console App project. Here you'll have to create another bin subfolder.
Recompile - this time it will build just fine.

In the debug folder, create a file called "report.aspx" with this content:
<%@ Page Language="C#" %>
<asp:Label runat="server" Font-Bold="true">3 + 2 is <%=3 + 2 %></asp:Label>

Run the project and enjoy the generated web page :)

What's happening there

As you may have noticed, the bulk of the work is done by the HttpRuntime.ProcessRequest call. However this call requires a specially-setup AppDomain, that our Console App does not have, so we have to create another AppDomain by the use of ApplicationHost.CreateApplicationHost.
An instance of the HostMarshaller class gets created in this new AppDomain, and in its Run method we launch the request processing.
To pass parameters to the web page, you can use the second parameter of the Run method, it is a standard query string in the form key=value&key=value...

That's it, have fun!