Cassini / WebServer.WebDev, NUnit и AppDomainUnloadedException - PullRequest
9 голосов
/ 18 февраля 2009

Я использую Cassini / WebServer.WebDev для запуска некоторых автоматических тестов веб-сервиса с использованием NUnit.

Я ничего необычного не делаю, просто

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
    }
  }
}

, но когда я запускаю его с помощью nunit-console.exe, я получаю следующий вывод:

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.

Если я запускаю nunit-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).

У кого-нибудь есть идеи, что может быть причиной этого?

1 Ответ

7 голосов
/ 21 июля 2009

У меня была такая же проблема, но я не использовал Кассини. Вместо этого у меня был собственный хостинг веб-сервера на базе System.Net.HttpListener с поддержкой ASP.Net через System.Web.HttpRuntime, работающий в другом домене приложения, созданном с помощью System.Web.Hosting.ApplicationHost.CreateApplicationHost(). По сути, именно так работает Cassini, за исключением того, что Cassini работает на уровне сокетов и реализует множество функций, предоставляемых самой System.Net.HttpListener.

В любом случае, чтобы решить мою проблему, мне нужно было позвонить System.Web.HttpRuntime.Close(), прежде чем позволить NUnit выгрузить домен моего приложения. Я сделал это, выставив новый метод Close() в моем прокси-классе хоста, который вызывается методом [TearDown] моего класса [SetupFixture], и ​​этот метод вызывает System.Web.HttpRuntime.Close().

Я посмотрел на реализацию Cassini через .Net Reflector и, хотя он использует System.Web.HttpRuntime.ProcessRequest(), похоже, он нигде не вызывает System.Web.HttpRuntime.Close().

Я не совсем уверен, как вы можете продолжать использовать предварительно собранную реализацию Cassini (Microsoft.VisualStudio.WebHost.Server), так как вам нужно, чтобы вызов System.Web.HttpRuntime.Close() происходил в домене приложения, созданном Cassini для размещения ASP.Net .

Для справки, вот некоторые части моего теста рабочего модуля со встроенным веб-хостингом.

Мой WebServerHost класс - это очень маленький класс, который позволяет маршалировать запросы в домен приложения, созданный 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));
    }
}

Класс WebServerContext - это просто оболочка вокруг System.Net.HttpListenerContext экземпляра, который наследуется от System.MarshalByRefObject, чтобы разрешить вызовам из нового хост-домена ASP.Net перезвонить в мой домен.

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;
}

Класс WebServerRequest является просто реализацией абстрактного класса System.Web.HttpWorkerRequest, который вызывает обратно в мой домен из домена размещения ASP.Net через класс WebServerContext.

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;
}

Класс WebServer - это контроллер для запуска и остановки веб-сервера. При запуске хост-домен ASP.Net создается с моим классом WebServerHost в качестве прокси-сервера для обеспечения взаимодействия. Также запускается экземпляр System.Net.HttpListener и запускается отдельный поток для приема соединений. Когда соединения установлены, рабочий поток запускается в пуле потоков для обработки запроса снова через мой класс WebServerHost. Наконец, когда веб-сервер останавливается, слушатель останавливается, контроллер ожидает, когда поток, принимающий соединения, завершится, и затем слушатель закрывается. Наконец, среда выполнения HTTP также закрывается посредством вызова метода WebServerHost.Close().

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));
    }
}

Наконец, этот класс Initialization, помеченный атрибутом NUnit [SetupFixture], используется для запуска веб-сервера при запуске модульных тестов и его выключения после завершения.

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();
    }
}

Я знаю, что это не совсем ответ на вопрос, но я надеюсь, что вы найдете эту информацию полезной.

...