ASP.NET CreateApplicationHost/SimpleWorkerRequest API Hole

Today, we ran into a bit of a pickle with running ASP.NET requests within a Windows Service process.  It’s a little known fact that ASP.NET can actually service requests outside of a Web environment.  In our case, we’re using it in a UI-less process, but all of the APIs work A-OK in a Windows Forms process.

 

We had a requirement in our software to be able to do some text processing.  We needed to be able to take a template and from that template generate HTML from a set of dynamic data.  We also needed to be able to produce XML from the same templates, and plain-text.  One approach would have been to write our own template processing engine. Another, which is the one we chose, was to use ASP.NET.  After all, all ASP does is transform a template (albeit a potentially highly complex template) into text output.

 

Hunting around a little, I eventually found an the SimpleWorkerRequest class, the HttpRuntime class and ProcessRequest method.  These two classes are required to actually process an ASP.NET page.  Here’s some code that runs a page called text.aspx and sends the output to a StringBuilder.

 

///

/// Runs a page and returns the results.

///

///

public string Run()

{

            // writer...

            StringWriter writer = new StringWriter();

 

            // request...

            SimpleWorkerRequest request = new SimpleWorkerRequest("test.aspx", string.Empty, writer);

 

            // run...

            HttpRuntime.ProcessRequest(request);

 

            // flush...

            writer.Flush();

 

            // show...

            return writer.GetStringBuilder().ToString();

}

 

HttpRuntime.ProcessRequest relies on certain pieces of context data being available in the AppDomain in which you’re executing.  This context data is not created inside a normal AppDomain – i.e. it’s not available in the main AppDomain that you get when you spin up a new process.  The ApplicationHost class contains a method called CreateApplicationHost.  This creates a new AppDomain and properly configures the bits of context data required for ProcessRequest to do its work.  If you don’t do this, ProcessRequest fails.

 

// create a domain...

HostMarshaller result = (HostMarshaller)ApplicationHost.CreateApplicationHost(typeof(HostMarshaller), "/", baseFolder.FullName);

 

// run...

string output = result.Run();

 

With it so far?  I shall assume so!  There are three big problems with this approach.

CreateApplicationHost is a nice, lightweight method.  I give it the type of a class which I want to create in the new AppDomain (I rightly or wrongly call this a “poke” class, because it allows me to poke at things in the other AppDomain.), the virtual folder path and the name of the folder containing the ASP.NET files.  As you know, ASP.NET will hunt around in a bin folder underneath this folder to find private assemblies.

CreateApplicationHost creates a new AppDomain.  In our case, we don’t need a new AppDomain – we already have one that should work just fine.  Yes, I accept there are situations where you would need a new AppDomain (proper ASP.NET, for example), but I don’t think this is necessary in every case when using ASP.NET outside of IIS.  In Whidbey, I would like to see a way of saying “fix up this current AppDomain for me, please”.

The next big problem is this.  What we do with our text processor is mark the .aspx page with a @Page directive that tells it to inherit from our own base class.  On this base class, we put properties that we want to make available to the script code.  These properties point to the data that the template is transforming into text. 

 

However, ProcessRequest is so atomic that we don’t get the opportunity to fiddle with the class before the request runs.  This means that we cannot set those properties.  Inside ProcessRequest, a new class is created that inherits from our base class, the page is rendered and the results output to the stream.  The page is then disposed as and when – but the point here is that we can’t set it up before the request starts.

 

What we do is rig the properties on our base page so that it pulls it out of some sort of context bucket (which we populate with our poke class).  This means that the properties “self initialize” themselves from context.  Now, the only way I think this will work is to provide an IDictionary-implementing object to ProcessRequest that contains a name/value collection of properties to set on the newly created class through Reflection.  This would make it easier to create the properties.  An alternative, potentially more elegant approach would be to let me implement an interface on my base class, with a method like “OK, now you can populate your properties on this instance that I’ve just given you”, which ASP.NET calls before the request is executed. 

 

The third huge problem that we’ve encountered is the fact that ASP.NET is fairly adamant in its desire to load binaries from the “bin” folder.  However, our application’s private assemblies are in its own bin folders somewhere on disk and we (and we’ve tried) can’t seem to marry these two up – ASP.NET seems to insist on trying to load our private assemblies from whereverOurAspxPagesAre/bin.  We don’t want to dump our .aspx pages in the application’s install root, which is what this approach implies.  

 

We did some up with a solution for this, but it’s not pretty (it’s a kinda crazy hack) and if no one else comes up with anything better than I’ll post it later. 

 

Personally, I only like putting things in the GAC if there’s really good reason to.  I guess this would fix the resolution problems.  The fact that I’m trying to use ASP.NET in ways that it was never designed for isn’t, to me at least, a good enough reason to put something in the GAC.

 

So there we have it, all those boys and girls out there working on Whidbey, this is what I want:

  • A way to fix up the AppDomain that I’m in so that ProcessSimpleRequest will work,
  • A way to populate properties on instances of my own base class before the request is processed, either by me giving you a dictionary, or you giving me the opportunity to do so through a callback,
  • A way of running ASP.NET (outside of IIS) without it having such strict rules on where that bin folder should be.  (I guess doing the first one will fix that?)

No Comments