Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cassini/WebServer.WebDev, NUnit and AppDomainUnloadedException

I am using Cassini/WebServer.WebDev to run some automated tests of a WebService using NUnit.

I am not doing anything fancy, just

public class WebService{
  Microsoft.VisualStudio.WebHost.Server _server;

  public void Start(){
    _server = new Microsoft.VisualStudio.WebHost.Server(_port, "/", _physicalPath);
  }

  public void Dispose()
  {
    if (_server != null)
    {
      _server.Stop();
      _server = null;
    }
  }
}
[TestFixture]
public void TestFixture{
  [Test]
  public void Test(){
    using(WebService webService = new WebService()){
      webService.Start();
      // actual test invoking the webservice
    }
  }
}

, but when I run it using nunit-console.exe, I get the following output:

NUnit version 2.5.0.9015 (Beta-2)
Copyright (C) 2002-2008 Charlie Poole.\r\nCopyright (C) 2002-2004 James W. Newki
rk, Michael C. Two, Alexei A. Vorontsov.\r\nCopyright (C) 2000-2002 Philip Craig
.\r\nAll Rights Reserved.

Runtime Environment -
   OS Version: Microsoft Windows NT 6.0.6001 Service Pack 1
  CLR Version: 2.0.50727.1434 ( Net 2.0.50727.1434 )

ProcessModel: Default    DomainUsage: Default
Execution Runtime: net-2.0.50727.1434
.....
Tests run: 5, Errors: 0, Failures: 0, Inconclusive: 0 Time: 28,4538451 seconds
  Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0


Unhandled exceptions:
1) TestCase1 : System.AppDomainUnloadedException: Attempted to access an unloaded AppDomain.
2) TestCase2 : System.AppDomainUnloadedException: Attempted to access an unloaded AppDomain.
3) TestCase3 : System.AppDomainUnloadedException: Attempted to access an unloaded AppDomain.
4) TestCase4 : System.AppDomainUnloadedException: Attempted to access an unloaded AppDomain.

If I run nunit-console under the debugger, I get the following output in the debug console:

[...]
The thread 0x1974 has exited with code 0 (0x0).
############################################################################
##############                 S U C C E S S               #################
############################################################################
Executed tests       : 5
Ignored tests        : 0
Failed tests         : 0
Unhandled exceptions : 4
Total time           : 25,7092944 seconds
############################################################################
The thread 0x1bd4 has exited with code 0 (0x0).
The thread 0x10f8 has exited with code 0 (0x0).
The thread '<No Name>' (0x1a80) has exited with code 0 (0x0).
A first chance exception of type 'System.AppDomainUnloadedException' occurred in System.Web.dll
##### Unhandled Exception while running 
System.AppDomainUnloadedException: Attempted to access an unloaded AppDomain.
   at System.Web.Hosting.ApplicationManager.HostingEnvironmentShutdownComplete(String appId, IApplicationHost appHost)
   at System.Web.Hosting.HostingEnvironment.OnAppDomainUnload(Object unusedObject, EventArgs unusedEventArgs)
A first chance exception of type 'System.Threading.ThreadAbortException' occurred in mscorlib.dll
A first chance exception of type 'System.Threading.ThreadAbortException' occurred in mscorlib.dll
A first chance exception of type 'System.Threading.ThreadAbortException' occurred in System.Web.dll
The thread 0x111c has exited with code 0 (0x0).
The program '[0x1A64] nunit-console.exe: Managed' has exited with code -100 (0xffffff9c).

Do anyone have any ideas what could be causing this?

like image 209
Rasmus Faber Avatar asked Feb 18 '09 14:02

Rasmus Faber


1 Answers

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.

like image 104
GBegen Avatar answered Nov 13 '22 20:11

GBegen