Assemblies zur Laufzeit ersetzen II
Um Assemblies zur Laufzeit zu ersetzen, d.h. eine neue Version (quasi) jederzeit einbinden zu können, müssen nicht mehr benötigte Assembly-Versionen aus dem Speicher entfernt werden. Darum ging es gestern.
Wie aber weiß ein Prozess, dass sich überhaupt eine Assembly verändert hat und er mit einer neuen Version arbeiten soll? Am einfachsten wäre es, wenn man die bisherige Assembly auf der Festplatte durch eine neue ersetzte. Das könnte der Prozess mit einem FileSystemWatcher feststellen:
private withevents _fsw as IO.FileSystemWatcher
...
_fsw = new IO.FileSystemWatcher("c:\myapp\bin", "myassembly.dll)
_fsw.NotifyFilter = IO.NotifyFilters.LastWrite
_fsw.EnableRaisingEvents = true
...
Private Sub _fsw_Changed(ByVal sender As Object, _
ByVal e As System.IO.FileSystemEventArgs) _
Handles _fsw.Changed
' tausche Assembly aus...
End Sub
Solange eine Assembly allerdings geladen ist, kann sie nicht auf der Festplatte ersetzt werden. Die Datei ist gesperrt.
Um das zu vermeiden, kann eine AppDomain jedoch angewiesen werden, Assemblies nicht im Original zu laden, sondern sie vorher zu kopieren. Die kopierten Assemblies nennt der .NET Framework "Shadow Copies". Sie werden für die Anwendung transparent in einem temporären Verzeichnis vor dem Laden erzeugt. Dort sind sie zwar immer noch gesperrt, aber das ist dann unerheblich.
Denn am ursprünglichen Ort können sie überschrieben werden.
Die Dokumentation zu Shadow Copies ist leider nicht ausführlich und so hat es mich einige Zeit gekostet, die "wahre Funktionsweise" herauszufinden.
Beim Erzeugen einer AppDomain werden ihr einige Pfade mitgeteilt, in denen sie zu ladende Assemblies suchen soll:
dim info as New AppDomainSetup()
with appdomain.CurrentDomain.SetupInformation
info.ApplicationBase = "c:\inetpub\wwwroot\myapplication"
info.PrivateBinPath = "bin"
end with
ApplicationBase ist das Wurzelverzeichnis der Anwendung, also meist das, wo die EXE-Datei liegt. Bei Web-Anwendungen ist es allerdings das Verzeichnis für die Virtual Root der Applikation.
PrivateBinPath enthält eine durch ";" getrennte Liste von relativen (!) Pfaden für Verzeichnisse, in denen unterhalb (!) der ApplicationBase vom CLR Lader auch nach Assemblies gesucht werden soll. Ist die Liste leer, dann werden nur Assemblies im ApplicationBase-Verzeichnis gefunden.
PrivateBinPath könnte also z.B. gesetzt werden auf ".", "bin" oder "bin;plug-ins", aber nicht auf "c:\mylibraries" oder "..".
Um mit Shadow Copies zu arbeiten, sind noch zwei weitere Eigenschaften einer AppDomain zu setzen. Mit
info.ShadowCopyFiles = "true"
wird der Mechanismus eingeschaltet. (Achtung: Der zugewiesene Wert ist ein String ("true" oder "false"), kein Boolean-Wert!)
Ohne weitere Angaben werden jetzt alle Assemblies in der ApplicationBase und in den PrivateBinPath-Verzeichnissen nur als Kopien geladen. D.h. in diesen Verzeichnissen sind sie nun nicht mehr gesperrt und können ersetzt werden.
Falls allerdings nur Assemblies aus einer Untermenge von PrivateBinPath-verzeichnissen als Shadow Copies geladen werden sollen, kann das über die Property ShadowCopyDirectories angegeben werden, z.B.:
info.ApplicationBase = "c:\myapplication"
info.PrivateBinPath = "plug-ins"
info.ShadowCopyDirectories = "c:\myapplication\plug-ins"
ShadowCopyDirectories ist optional und nimmt auch wieder eine Liste von ";"-separierten Verzeichnissen auf. Aber Achtung: Hier sind absolute (!) Pfade anzugeben. Leider ist das der .NET Framework Dokumentation nicht zu entnehmen und es hat mich einige Zeit gekostet, darauf zu kommen, als meine Tests nicht funktionierten.
Die Schritte für den Austausch von Assemblies zur Laufzeit - so wie es in ASP.NET funktioniert - sind damit klar:
-Assemblies in eine separate AppDomain laden und nur über ein Proxy-Objekt darauf zugreifen.
-AppDomain mit Shadow Copies arbeiten lassen.
-FileSystemWatcher einrichten, um festzustellen, wann sich Assemblies verändert haben.
-AppDomain entladen und neu aufbauen mit veränderten Assemblies.
Die Realisierung gerade des letzten Schrittes ist aber natürlich nicht trivial. Wie kann Ab- und Aufbau einer AppDomain vor Client-Code verborgen werden? Auch das Proxy-Objekt wird ja verworfen. Die Entkopplung muss also eigentlich noch stärker sein. Vielleicht über einen TCP/IP-Kanal?
Und auch noch andere Fragen sind zu klären: Wie können mehrere zusammengehörige Assemblies ausgetauscht werden? (Das ist selbst bei ASP.NET nicht wirklich möglich.) Und was soll passieren, wenn neue Assembly-Versionen eigentlich einen Anwendungsneustart erfordern?
Hm... darüber muss ich ein andermal nachdenken.