Cassini/WebServer.WebDev, NUnit and AppDomainUnloadedException

梦想与她 提交于 2019-12-03 06:24:20

I had the same problem, but was not using Cassini. Instead, I had my own web server hosting based on System.Net.HttpListener with ASP.Net support through System.Web.HttpRuntime running in a different application domain created via System.Web.Hosting.ApplicationHost.CreateApplicationHost(). This is essentially the way Cassini works, except that Cassini works at the socket layer and implements a lot of the functionality provided by System.Net.HttpListener itself.

Anyway, to solve my problem, I needed to call System.Web.HttpRuntime.Close() before letting NUnit unload my application domain. I did this by exposing a new Close() method in my host proxy class that is invoked by the [TearDown] method of my [SetupFixture] class and that method calls System.Web.HttpRuntime.Close().

I looked at the Cassini implementation through .Net Reflector and, although it uses System.Web.HttpRuntime.ProcessRequest(), it doesn't seem to call System.Web.HttpRuntime.Close() anywhere.

I'm not exactly sure how you can keep using the pre-built Cassini implementation (Microsoft.VisualStudio.WebHost.Server), as you need to get the System.Web.HttpRuntime.Close() call to occur within the application domain created by Cassini to host ASP.Net.

For reference, here are some pieces of my working unit test with embedded web hosting.

My WebServerHost class is a very small class that allows marshaling requests into the application domain created by System.Web.Hosting.ApplicationHost.CreateApplicationHost().

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

public class WebServerHost :
    MarshalByRefObject
{
    public void
    Close()
    {
        HttpRuntime.Close();
    }

    public void
    ProcessRequest(WebServerContext context)
    {
        HttpRuntime.ProcessRequest(new WebServerRequest(context));
    }
}

The WebServerContext class is simply a wrapper around a System.Net.HttpListenerContext instance that derives from System.MarshalByRefObject to allow calls from the new ASP.Net hosting domain to call back into my domain.

using System;
using System.Net;

public class WebServerContext :
    MarshalByRefObject
{
    public
    WebServerContext(HttpListenerContext context)
    {
        this.context = context;
    }

    //  public methods and properties that forward to HttpListenerContext omitted

    private HttpListenerContext
    context;
}

The WebServerRequest class is just an implementation of the abstract System.Web.HttpWorkerRequest class that calls back into my domain from the ASP.Net hosting domain via the WebServerContext class.

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

class WebServerRequest :
    HttpWorkerRequest
{
    public
    WebServerRequest(WebServerContext context)
    {
        this.context = context;
    }

    //  implementation of HttpWorkerRequest methods omitted; they all just call
    //  methods and properties on context

    private WebServerContext
    context;
}

The WebServer class is a controller for starting and stopping the web server. When started, the ASP.Net hosting domain is created with my WebServerHost class as a proxy to allow interaction. A System.Net.HttpListener instance is also started and a separate thread is started to accept connections. When connections are made, a worker thread is started in the thread pool to handle the request, again via my WebServerHost class. Finally, when the web server is stopped, the listener is stopped, the controller waits for the thread accepting connections to exit, and then the listener is closed. Finally, the HTTP runtime is also closed via a call into the WebServerHost.Close() method.

using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Web.Hosting;

class WebServer
{
    public static void
    Start()
    {
        lock ( typeof(WebServer) )
        {
            //  do not start more than once
            if ( listener != null )
                return;

            //  create web server host in new AppDomain
            host =
                (WebServerHost)ApplicationHost.CreateApplicationHost
                (
                    typeof(WebServerHost),
                    "/",
                    Path.GetTempPath()
                );

            //  start up the HTTP listener
            listener = new HttpListener();
            listener.Prefixes.Add("http://*:8182/");
            listener.Start();

            acceptConnectionsThread = new Thread(acceptConnections);
            acceptConnectionsThread.Start();
        }
    }

    public static void
    Stop()
    {
        lock ( typeof(WebServer) )
        {
            if ( listener == null )
                return;

            //  stop listening; will cause HttpListenerException in thread blocked on GetContext()  
            listener.Stop();

            //  wait connection acceptance thread to exit
            acceptConnectionsThread.Join();
            acceptConnectionsThread = null;

            //  close listener
            listener.Close(); 
            listener = null;

            //  close host
            host.Close();
            host = null;
        }
    }

    private static WebServerHost
    host = null;

    private static HttpListener
    listener = null;

    private static Thread
    acceptConnectionsThread;

    private static void
    acceptConnections(object state)
    {
        while ( listener.IsListening )
        {
            try
            {
                HttpListenerContext context = listener.GetContext();
                ThreadPool.QueueUserWorkItem(handleConnection, context);
            }
            catch ( HttpListenerException e )
            {
                //  this exception is ignored; it will be thrown when web server is stopped and at that time
                //  listening will be set to false which will end the loop and the thread
            }
        }
    }

    private static void
    handleConnection(object state)
    {
        host.ProcessRequest(new WebServerContext((HttpListenerContext)state));
    }
}

Finally, this Initialization class, marked with the NUnit [SetupFixture] attribute, is used to start the web server when the unit tests are started, and shut it down when they are completed.

using System;
using NUnit.Framework;

[SetUpFixture]
public class Initialization
{
    [SetUp]
    public void
    Setup()
    {
        //  start the local web server
        WebServer.Start();
    }

    [TearDown]
    public void
    TearDown()
    {
        //  stop the local web server
        WebServer.Stop();
    }
}

I know that this is not exactly answering the question, but I hope you find the information useful.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!