Hosting ASP.NET Outside of IIS

来源:互联网 发布:剑灵人男捏脸数据邪气 编辑:程序博客网 时间:2024/06/04 20:02
Hosting ASP.NET Outside of IIS
http://hyperthink.net/blog/permalink.aspx?guid=271632d2-07e3-41af-9e58-9a7e25348b8c

One of the things that bugs me about writing ASP.NET code is that there's no simple way to unit test the pages that you create. Even if you're a Good Programmer and abstract your business logic away from the Page object itself (where it can be more easily unit tested), you still end up with a lot of code in your code-behind files. This code is a pain to test, because the only good way to test it is by firing up the page in the browser and entering in some data.

There are a couple of existing tools that do this sort of thing. Rational Robot is good for doing live site testing, but it's expensive and hard to learn. NUnitAsp is another solution, but it works by parsing the returned HTML response. As a result, it only supports a limited set of controls and still requires you to have IIS up and running, which complicates testing (especially in environments where the build machine's configuration is markedly different from the development machine -- you want to easily be able to run unit tests on both).

What I wanted to be able to do was say page = new MyPageClass() inside of an NUnit test and be done with it. While this is syntactically valid (it will compile), the returned object won't be very useful. None of the controls will be instantiated (let alone initialized), and as a result many important collections (like Controls and Validators) will be empty. It turns out that there's no easy way to do what I wanted to do -- all this initialization is handled by the HttpRuntime infrastructure, and there's no straightforward way to do it outside of there. So, since eliminating the HttpRuntime was not an option, I decided to host the HttpRuntime inside of an object of my own devising by using System.Web.Hosting namespace.

The result of this effort was the AspNetNUnitHost class, the source code for which is linked to at the bottom of this message. This class is a custom ASP.NET application host, and can process HttpRequests through the ASP.NET application pipeline -- just like ASP.NET, only without IIS and HTTP!

In order to make this work, I had to resort to some fairly ugly trickery. The ultimate goal was to set things up in such a way so that I could call PageParser.GetCompiledPageInstance() to get back an instance of that Page object that I wanted to unit test. In order to do that, I had to find a way to initalize the HttpRuntime.

The HttpRuntime is highly tuned to support an intialize-on-first-request pattern, and as such does not expose any public initialization functions. The only way to do it was to create an instance of SimpleWorkerRequest to fake an incoming HTTP request, build an HttpContext instance out of that request, and then call HttpRuntime.ProcessRequest() on that object. Once that's done, I could call PageParser.GetCompiledPageInstance() and have it succeed -- or so I thought.

GetCompiledPageInstance() has some calls to the static HttpContext.Current property inside of it. Since I wasn't really running under the runtime, I had to fake this by dropping my own instance of HttpContext into the LogicalCallContext under a key that was known to the runtime (I got this key by decompiling System.Web.dll). That got GetCompiledPageInstance to succeed, so I could then call ProcessRequest() on that compiled instance to initialize the page.

The really nifty parts of this code look like this:

SimpleWorkerRequest request = new SimpleWorkerRequest(page, null,
                                                   _outputWriter);
HttpContext context = new HttpContext(request);

//The HttpRuntime does not have any initialization
//functions -- everything happens
//during the servicing of the first request.

if(AspNetNUnitHost.RUNTIME_INITIALIZED)
{
   //This call has the side effect of initializing the HTTP runtime
   HttpRuntime.ProcessRequest(request);
   AspNetNUnitHost.RUNTIME_INITIALIZED = true;
}

//Drop an HttpContext instance onto the message,
//so that subsequent calls to
//the static HttpContext.Current will return a context
System.Runtime.Remoting.Messaging.CallContext.SetData("HttpContext",
                                                       context);

//Get the compiled page instance
o = PageParser.GetCompiledPageInstance(_virDir + "/" + page,
                                       _physicalDir + "//" + page,
                                       context);

//This has the side effect of intializing the Page object
//(instantiating all its controls and populating
//the Controls collection, for example)
((IHttpHandler)o).ProcessRequest(context);

From there on out, I had a working page instance that I could then unit test. Since that returned instance lived in another AppDomain than NUnit, I wrapped it in a MarshalByRef wrapper to ensure that it could successfully be passed back to the unit test code across the AppDomain boundary.

Inside of my unit test code, I could then do something like this:

AspNetNunitHost host = AspNetNunitHost.CreateAspNetNunitHost("/foo",
                                          @"c:/inetpub/wwwroot/foo",
                                          Console.Out);

AspNetControlWrapper o = host.GetCompiledPageInstance("MyPage.aspx");
I'm sure there are easier ways to unit test ASP.NET pages...I started out with an idea, and then just kept going to see if I could do it. Regardless, the full source for this is attached below.

//File:    AspNetNUnitHost.cs
//Author:  Steve Maine
//Email:   stevem@hyperthink.net


// This was compiled and tested against ASP.NET 1.0. I make no assertions or guarantees that this
// will work on any other version of the framework, since there's a lot of wierd stuff going on...

using System;
using System.Diagnostics;
using System.Web;
using System.Web.Hosting;
using System.Web.UI;

namespace Maine.Steve.UnitTests
{
    //This is a simple MarshalByRef wrapper for the returned control.
    //Necessary because the control will be instantiated in a different AppDomain
    //than the test code, and TemplateControl is not MarshalByRef.
    public class TemplateControlWrapper : MarshalByRefObject
    {
        public System.Web.UI.TemplateControl Control;

        public TemplateControlWrapper(System.Web.UI.TemplateControl control)
        {
            Control = control;
        }
    }

    //Host the ASP.NET runtime outside of IIS.
    [Serializable]
    public class AspNetNUnitHost : MarshalByRefObject, IDisposable
    {
        //The virtual directory name of the web app.
        public string               _virDir;

        //The physical directory to which that VRoot maps
        public string               _physicalDir;

        //Where the output goes
        public System.IO.TextWriter _outputWriter;

        //The physical path to the aspnet_isapi DLL
        public static string        ASPNET_ISAPI_PATH = @"c:/windows/Microsoft.NET/Framework/v1.0.3705/aspnet_isapi.dll";

        //Has the HttpRuntime already been initialized?
        public static bool RUNTIME_INITIALIZED = false;

        private TextWriterTraceListener consoleListener;
        private static long hModule;

        //PInvoke decl for LoadLibrary
        [System.Runtime.InteropServices.DllImport("KERNEL32.DLL", EntryPoint = "LoadLibraryEx")]
        public static extern long LoadLibrary(string fileName, long hFile, long dwFlags);

        //PInvoke decl for FreeLibrary
        [System.Runtime.InteropServices.DllImport("KERNEL32.DLL", EntryPoint="FreeLibrary")]
        public static extern bool FreeLibrary( long hModule );

        //Creates a host that can serve pages from a particular VRoot
        public static AspNetNUnitHost CreateAspNetNunitHost(string virDir, string physicalDir, System.IO.TextWriter writer)
        {
            //Load up aspnet_isapi, because some of the code that initializes the HttpRuntim
            //depends on functions defined in this library.
            hModule = LoadLibrary(ASPNET_ISAPI_PATH, 0, 0);

            //Create an ASP.NET Application Host
            AspNetNUnitHost host = (AspNetNUnitHost) ApplicationHost.CreateApplicationHost(typeof(AspNetNUnitHost), virDir, physicalDir);
          
            host._physicalDir  = physicalDir;
            host._virDir       = virDir;
            host._outputWriter = writer;

            return host;
        }

        public AspNetNUnitHost()
        {
        }
                       
        //Fakes an incoming page request (i.e. "foo.aspx")
        public void ProcessRequest(string page)
        {
            HttpRuntime.ProcessRequest(new SimpleWorkerRequest(page, null, this._outputWriter));
        }

        //Returns a compiled instance of the ASP.NET Page object.
        //The returned instance is wrapped in an AspNetControlWrapper, after which
        //the page can be accessed just like any other object.
        public TemplateControlWrapper GetCompiledPageInstance(string page)
        {
            object o = null;

            try
            {
                SimpleWorkerRequest request = new SimpleWorkerRequest(page, null, _outputWriter);
                HttpContext context = new HttpContext(request);

                //The HttpRuntime does not have any initialization functions -- everything happens
                //during the servicing of the first request.
                if(AspNetNUnitHost.RUNTIME_INITIALIZED)
                {
                    //This call has the side effect of initializing the HTTP runtime
                    HttpRuntime.ProcessRequest(request);
                    AspNetNUnitHost.RUNTIME_INITIALIZED = true;
                }
               
                //Drop an HttpContext instance onto the message, so that subsequent calls to
                //the static HttpContext.Current will return a context
                System.Runtime.Remoting.Messaging.CallContext.SetData("HttpContext", context);
               
                //Get the compiled page instance
                o = PageParser.GetCompiledPageInstance(_virDir + "/" + page, _physicalDir + "//" + page, context);
               
                //This has the side effect of intializing the Page object
                //(instantiating all its controls and populating the Controls collection, for example)
                ((IHttpHandler)o).ProcessRequest(context);   

            }
            catch(Exception e)
            {
                Debug.WriteLine(e.GetType().ToString());
                Debug.WriteLine(e.Message);
               
                Debug.WriteLine(e.StackTrace);
            }

            //Wrap the control in a MarshalByRef so it can marshal back to the AppDomain
            //of the caller.
            return new TemplateControlWrapper((TemplateControl) o);
        }

        #region IDisposable Members

        public void Dispose()
        {
            Dispose( false );
        }
       
        ~AspNetNUnitHost()
        {
            Dispose( true );
        }

        private void Dispose( bool bFinalizing )
        {
            if( !bFinalizing )
                GC.SuppressFinalize( this );

            //Free the aspnet_isapi .dll
            FreeLibrary( AspNetNUnitHost.hModule );
        }

        #endregion
    }

    //Example of usage in a unit test:
    //AspNetNunitHost host = AspNetNunitHost.CreateAspNetNunitHost("/foo", @"c:/inetpub/wwwroot/foo", Console.Out);
    //AspNetControlWrapper o = host.GetCompiledPageInstance("MyPage.aspx");
}


相关文章
http://blog.csdn.net/anghlq/archive/2007/04/19/1571100.aspx