ASP.NET-Runtime in die eigene Applikation einbinden

Im Laufe der letzten Jahre habe ich des Öfteren die ASP.NET-Runtime in Desktop-Applikationen integriert, sei es um ein komplexes HTML-Reporting umzusetzen oder um eine .NET 2.0-Anwendung Web Services veröffentlichen zu lassen. Nicht zuletzt der Wunsch nach einem kleinen, „eigenen Webserver“ hat meine Beschäftigung mit dem Thema vorangetrieben. Im Folgenden will ich kurz dokumentieren, was sich bis heute als meine „Best Practice“-Lösung herauskristallisiert hat.

Die wichtigste Erkenntnis war für mich, dass die Runtime in eine eigene AppDomain geladen werden muss und so von der restlichen Anwendung isoliert wird – das hat Vor- und Nachteile (bspw. kann die Runtime-AppDomain nach Gebrauch einfach entladen werden, auf der anderen Seite müssen Daten aus der Anwendung über eine AppDomain-Grenze transportiert werden, um im „Web-Bereich“ genutzt werden zu können). Es gibt zwar Mittel und Wege eine vorhandene AppDomain so zu manipulieren, dass sie die Runtime aufnehmen und ausführen kann, aber in der Praxis hat sich dies für mich nicht als sinnvoll erwiesen.

Laden der ASP.NET-Runtime

Das Laden der ASP.NET-Runtime beginnt mit der Klasse ApplicationManager aus dem Namespace System.Web.Hosting:

ApplicationManager appManager = ApplicationManager.GetApplicationManager();

Ist der Rückgabewert ungleich null,  kann in diesem Prozess mit dem Laden einer Ausführungsumgebung für eine Webanwendung begonnen werden.

Jetzt ist es prinzipiell nicht mehr schwer, einen physischen Pfad (das Basisverzeichnis der Webanwendung auf der Festplatte) auf einen virtuellen (der Pfad hinter der Adresse des Servers, z.B. http://domain.tld/virtual) abzubilden – dazu muss lediglich der Typ eine Objektes her, der IRegisteredObject implementiert, und mit dessen Instanz man später kommunizieren möchte, um die Runtime die gewünschten Aufgaben erledigen zu lassen. Sinnvollerweise sollte dieser Typ von MarshalByRefObject abgeleitet sein, da die Runtime in einer anderen AppDomain ausgeführt wird (siehe oben). Es besteht jetzt allerdings das Problem, dass dieser Typ sowohl in der aktuellen AppDomain, als auch in der ASP.NET-AppDomain bekannt sein muss, sein Assembly also in beiden AppDomains geladen ist/geladen werden kann. Dies ließe sich zum Beispiel dadurch bewerkstelligen, dass die entsprechende Assembly im bin-Verzeichnis der auszuführenden Webanwendung abgelegt wird oder aber jetzt hineinkopiert wird. Das finde ich allerdings wenig elegant und glücklicherweise gibt es einen anderen Weg:  System.Web.Compilation.BuildManagerHost. Dieser Typ wird nicht exportiert und kann deswegen nicht direkt zugegriffen werden. Da er aber Teil der Runtime ist, kann diese auf ihn zugreifen und ihn instanzieren:

Type buildManagerHostType = typeof(HttpRuntime).Assembly.GetType(
    "System.Web.Compilation.BuildManagerHost");
Object buildManagerHost = appManager.CreateObject(
    appId,
    buildManagerHostType,
    virtualPath,
    physicalPath,
    failIfExists);

Dabei bezeichnet die appId die auszuführende Anwendung eindeutig, man könnte also der Einfachheit halber bspw. eine Verknüpfung aus physischem und virtuellem Pfad als appId verwenden. Der Parameter failIfExists gibt desweiteren an, wie damit umgegangen werden soll, wenn das gewünschte Objekt schon existiert: bei true wird eine Ausnahme ausgelöst und bei false das bereits existierende Objekt zurückgegeben.

Die AppDomain ist nun bereit, die Runtime geladen…

Injizieren eigener Assemblies in die ASP.NET-Laufzeitumgebung

Dabei hilft nun die Methode RegisterAssembly() des zuvor registrierten BuildManagerHost:

Assembly myAssembly = typeof(MyApplicationHost).Assembly;
buildManagerHostType.InvokeMember(
    "RegisterAssembly",
    BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
    null,
    buildManagerHost,
    new Object[] { myAssembly.FullName, myAssembly.Location });

Der Typ MyApplicationHost ist nun die oben erwähnte Klasse, die uns später das Steuern der Runtime ermöglichen wird. Diese Klasse sollte von MarshalByRefObject abgeleitet sein und muss IRegisteredObject implementieren.

MyApplicationHost myHost = (MyApplicationHost)appManager.CreateObject(
    appId,
    typeof(MyApplicationHost),
    virtualPath,
    physicalPath,
    failIfExists);

In einem Folgeartikel zeige ich dann, wie nun Aufrufe an die Runtime durchgeführt werden.