Administration von Typen und Anwendungen unter .NET (Teil 2)

Veröffentlicht: 07. Sep 2001 | Aktualisiert: 15. Jun 2004
Von Jeffrey Richter

Im Teil 1 dieser kleinen Artikelserie habe ich beschrieben, wie die .NET-Compiler von Microsoft Metadaten und MSIL-Code generieren (Microsoft Intermediate Language). Außerdem habe ich Ihnen vorgeführt, wie man diese Metadaten mit dem ILDasm.exe aus dem .NET Framework SDK untersuchen kann. Selbstverständlich war auch davon die Rede, was eigentlich ein "Assembly" (eine Assembly) ist, wie man private Assemblies erstellt und wie ein Administrator die Anwendung konfiguriert, damit die gemeinsame Laufzeitschicht für alle Sprachen (CLR) diese Assemblies findet.

In diesem Heft möchte ich darauf eingehen, wie man eine Assembly erstellt, das von mehreren Anwendungen benutzt werden kann. Sie werden erfahren, wie die Versionierung der Assemblies funktioniert und welche Rolle die CLR (Common Language Runtime) bei der Versionierung spielt.
Dieser Artikel beruht auf der Beta 1 von .NET. Es kann also noch zu beträchtlichen Änderungen der hier beschriebenen Techniken kommen.

Auf dieser Seite

Shared Assemblies Shared Assemblies
Assemblies mit starken Namen Assemblies mit starken Namen
Verzögerte Signatur Verzögerte Signatur
Versionsnummern Versionsnummern
Aufbau der Versionsnummern Aufbau der Versionsnummern
Auch Assemblies haben eine Kultur Auch Assemblies haben eine Kultur
Friedliches nebeneinander Friedliches nebeneinander
Auflösung von Typreferenzen Auflösung von Typreferenzen
Fortgeschrittene Administrative Kontrolle (Konfiguration) Fortgeschrittene Administrative Kontrolle (Konfiguration)
Fazit Fazit

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

sys.gif

Shared Assemblies

Die Laufzeitschicht unterscheidet zwischen zwei verschiedenen Arten von Assemblies, nämlich private und gemeinsame (shared). Von den privaten Assemblies war im letzten Heft die Rede. Private Assemblies werden, um es kurz zu wiederholen, zusammen mit einer bestimmten Anwendung ausgeliefert und ausschließlich von dieser Anwendung benutzt. Im Normalfall werden private Assemblies von derselben Firma geschrieben, die auch die Anwendung entwickelt. Somit hat diese Firma praktisch die völlige Kontrolle über die Namensgebung, die Versionen und das Verhalten des Assembly. Daher macht die Laufzeitschicht den privaten Assemblies auch keine administrativen Vorschriften.

Gemeinsame Assemblies (shared assemblies) werden speziell für die Benutzung durch mehrere Anwendungen entworfen. Oft wird solch ein gemeinsames Assembly von einer Firma entwickelt und von einer ganz anderen benutzt. Die Basisklassenbibliothek aus dem .NET Framework ist eine gutes Beispiel für eine gemeinsam benutztes Assembly, denn praktisch alle von .NET verwalteten Anwendungen werden Teile von ihr benutzen. An diesem Punkt tauchen aber auch Versionsprobleme auf. In diesem Abschnitt möchte ich die Erstellung eines gemeinsamen Assembly beschreiben und auf die Vorgaben der Laufzeitschicht eingehen, von denen es abhängt, sowie auf die Anbindung einer Anwendung an eine gemeinsame Assembly.

Der wichtigste Punkt dürfte wohl sein, dass private und gemeinsame Assemblies praktisch dieselbe Struktur haben. Sie benutzen dasselbe PE-Dateiformat, dieselben Metadaten und dieselben Manifesttabellen, von denen schon in der letzten Folge die Rede war. Und Sie als Entwickler benutzen für die gemeinsamen Assemblies dieselben Werkzeuge wie für die privaten Assemblies, zum Beispiel den C#-Compiler und AL.exe. Die wichtigen Unterschiede zwischen privaten und gemeinsamen Assemblies zeigen sich in der Namensgebung, der Versionierung und der Unterbringung auf der Maschine des Anwenders. Außerdem hat die Laufzeitschicht noch ein Wörtchen mitzureden, sobald eine Anwendung ein gemeinsames Assembly einbinden will.

Private Assemblies werden am Namen der PE-Datei identifiziert (ohne Erweiterung), in der das Manifest liegt. Für private Assemblies reicht diese einfache Methode völlig aus, denn im Normalfall werden alle Assemblies mit der Anwendung zusammen verpackt, vertrieben und installiert. Gemeinsam benutzte Assemblies werden dagegen von verschiedenen Firmen entwickelt, die ihre Entwicklungen höchstwahrscheinlich nicht aufeinander abstimmen. Das bedeutet, dass zwei verschiedene Firmen ihren völlig verschiedenen Assemblies durchaus identische Namen geben könnten. Solche Assemblies würde man auf den herkömmlichen Windows-Maschinen nur dann freiwillig installieren, wenn man Probleme liebt.

Damit gemeinsame Assemblies von Nutzen sind, müssen sie so auf der Festplatte untergebracht werden, dass die Laufzeitschicht sie leicht finden kann. Der dafür vorgesehene Ort ist der Global Assembly Cache (GAC). Er liegt normalerweise im Verzeichnis C:\Winnt\Assembly. Während der Entwicklung und der Testphase können Sie gemeinsame Assemblies mit einem Hilfsprogramm explizit im GAC installieren, zum Beispiel mit dem AL.exe. Es hat für diesen Zweck den Kommandozeilenschalter /I[nstall]:Dateiname.

Sie können auch das Programm GacUtil.exe aus dem .NET Framework SDK verwenden. Startet man es ohne weitere Argumente auf der Kommandozeile, so zeigt es seine Gebrauchsanweisung an (Listing L1).

L1 GAC gibt Hilfestellung für den eigenen Aufruf

Microsoft (R) .NET Global Assembly Cache Utility.  Version 1.0.2204.21 
Copyright (C) Microsoft Corp. 1998-2000 
Aufruf: Gacutil <Optionen> [ 
] 
 Optionen: 
  -I 
    Installiert eine Assembly im Global Assembly 
    Cache. Geben Sie den Namen der Manifestdatei als  
    Parameter an.  
    Beispiel:  -i myDll.dll 
  -u 
    Entfernt eine Assembly aus dem Global Assembly 
    Cache (uninstall). Geben Sie den Namen der zu ent- 
    fernenden Assembly als Parameter an. 
    Beispiele: 
      -u myDll 
      -u myDll,Ver=1.1.0.0,Loc=en,PK=874e23ab874e23ab 
  -l 
    Listet den Inhalt des Global Assembly Cache auf. 
  -? 
    Zeigt diesen Hilfetext an.

Wenn ein gemeinsames Assembly in einer .cab-Datei untergebracht oder in irgendeiner Form komprimiert wurde, muss die Datei vor der Installation des Assembly im GAC erst entkomprimiert werden. Die bei diesem Vorgang entstehenden temporären Dateien können nach der Installation des Assembly gelöscht werden. Das Windows-Installationsprogramm kann ab Version 1.5 auch mit MSI-Dateien umgehen, die Assemblies im GAC unterbringen wollen. (Durch den Start von MSIExec.exe finden Sie leicht heraus, welche Version des Installationsprogramms bei Ihnen installiert ist.) Man darf wohl davon ausgehen, dass die Installation von Assemblies im GAC in Zukunft hauptsächlich über MSI-Dateien erfolgen wird.

Die Installation der Assembly-Dateien in den GAC stellt eine Art Registrierung des Assembly dar und verletzt eigentlich das Ziel, die Installation, Sicherung, Verlagerung und Entfernung der Anwendung so einfach wie möglich zu gestalten. "Simpel" bleibt die Sache aber nur, wenn Sie ausschließlich private Assemblies einsetzen.

Welchen Sinn hat es nun, eine Assembly im GAC anzumelden? Nun, zwei verschiedene Firmen könnten auf die Idee kommen, ein Mathematik-Assembly zu entwickeln, das jeweils in einer einzigen Datei namens Calculus.dll untergebracht wird. Diese beiden Dateien darf man offensichtlich nicht in ein gemeinsames Verzeichnis kopieren, weil die zweite immer die erste überschreiben würde - sicher nicht zur Freude aller beteiligten Anwendungen. Wenn Sie ein Assembly aber mit Hilfe eines entsprechenden Hilfsprogramms im GAC unterbringen, legt dieses Programm ein Unterverzeichnis unter C:\Winnt\Assembly an und kopiert die Dateien des Assembly in dieses Unterverzeichnis. Der Name dieses Unterverzeichnisses wird mit einem speziellen Algorithmus generiert, der dafür sorgen soll, dass sich keine Duplikate ergeben. So gibt es auf meiner Maschine zum Beispiel ein Unterverzeichnis namens C96NCDM7. Im Normalfall interessiert sich aber niemand für diese Verzeichnisse. Der tatsächliche Verzeichnisname spielt also praktisch keine Rolle.

Wenn Sie sich das Assembly-Verzeichnis im Explorer ansehen, setzt der Explorer zu dessen Darstellung eine spezielle Erweiterung ein. Auf meiner Maschine sieht das Verzeichnis C:\Winnt\Assembly im Explorer zum Beispiel so aus, wie Bild B1 es zeigt. Ich habe mit der rechten Maustaste die Assembly System.WinForms angeklickt, damit sich das Kontextmenü öffnet. Seien Sie vorsichtig - durch die Wahl des Befehls Delete verschwindet das Assembly aus dem GAC. Sie können ein Assembly auch mit dem Hilfsprogramm GacUtil.exe löschen.

02-Admin01.gif

B1 Der Assembly Cache.

Der Menüpunkt Properties öffnet einen Eigenschaftsdialog (Bild B2). Aus der Zeitangabe über die letzte Änderung geht hervor, wann das Assembly in den GAC aufgenommen wurde. Und wenn ich die Plattformseite aufschlage, präsentiert sich der Dialog wie in Bild B3.

02-Admin02.gif

B2 Die Eigenschaften von System.WinForms.

02-Admin03.gif

B3 Die Plattformattribute des Assembly.

Der GAC merkt sich in einer kleinen Datenbank, wo die Assemblies zu finden sind. Eingetragen wird jeweils der starke Name des Assembly und das Unterverzeichnis, in dem die betreffenden Dateien liegen. Daraus ergibt sich zwanglos folgende Frage: "Was ist ein starker Name?" Nun, jede Firma, die ein gemeinsames Assembly herstellt, braucht eine Methode, nach der sich dieses Assembly eindeutig identifizieren lässt. Also musste Microsoft einen passenden Mechanismus vorsehen, mit dem das möglich wird. Microsoft hat dazu das Verfahren der Public Key Encryption gewählt, das manche Leser von der Verschlüsselung und Signierung von E-Mails her kennen dürften.

Assemblies mit starken Namen

Um einem Assembly einen starken Namen geben zu können, braucht man zuerst ein Schlüsselpaar mit öffentlichem und privatem Schlüssel. Dieses Schlüsselpaar dient zur Kennzeichnung der Assembly-Datei.

Wenn Sie einem Assembly einen starken Namen geben, erhält die FileDef-Tabelle im Manifest eine Liste aller Dateien, aus denen sich das Assembly zusammensetzt. Beim Eintrag der Dateinamen ins Manifest wird eine Prüfsumme über jede eingetragene Dateie errechnet und zusammen mit dem Dateinamen in die FileDef-Tabelle eingetragen. Den Prüfsummenalgorithmus können Sie übrigens auf zwei Wegen überschreiben: einmal mit dem Schalter /algid von AL.exe oder auf Assembliesebene mit dem anwenderdefinierten Attribut System.Reflection.AssemblyAlgIDAttribute.

Sobald die PE-Datei fertig ist, in der das Manifest liegt, wird eine Prüfsumme über die gesamte Datei gebildet (Bild B4). Als Prüfsummenalgorithmus wird immer SHA-1 eingesetzt. Der Algorithmus lässt sich auch nicht überschreiben. Die resultierende Prüfsumme, die um die 100-200 Bytes groß ist, wird mit dem privaten Schlüssel des Herausgebers signiert und die resultierende digitale RSA-Signatur wird in einem reservierten Abschnitt der PE-Datei untergebracht, der natürlich nicht in die Prüfsummenberechnung einbezogen wird. Dann wird der CLR-Kopf der PE-Datei aktualisiert, damit er die richtige Stelle angibt, an der die digitale Signatur in der Datei zu finden ist.

02-Admin04.gif

B4 Die Signierung eines Assembly

Auch der öffentliche Schlüssel des Herausgebers wird in dieser PE-Datei untergebracht, und zwar in der Metadaten-Tabelle AssemblyDef des Manifests. Durch die Kombination von Dateinamen und öffentlichem Schlüssel erhält dieses Assembly einen "starken Namen", der nach allem menschlichen Ermessen unverwechselbar ist. Solange alles mit rechten Dingen zugeht und die Firmen nicht dasselbe Schlüsselpaar benutzen, können zwei verschiedene Firmen einfach kein Calculus-Assembly mit demselben öffentlichen Schlüssel versehen.

An diesem Punkt ist das Assembly vertriebsbereit.
Öffentliche Schlüssel werden durch eine große Anzahl von Bytes dargestellt. Im Fall der AssemblyRef-Tabelle muss mit jedem angesprochenen Assembly ein öffentlicher Schlüssel verknüpft werden. Das bedeutet, dass ein relativ großer Anteil der Datei von den öffentlichen Schlüsseln beansprucht würde.
Um Platz zu sparen, bildet Microsoft eine Prüfsumme über den öffentlichen Schlüssel und verwendet nur die letzten acht Bytes dieser Prüfsumme. Dieser verkürzte Wert ist unter statistischen Gesichtspunkten hinreichend unverwechselbar und kann daher im System verwendet werden. Die solchermaßen verkürzten öffentlichen Schlüssel, auch "öffentliche Schlüssel-Symbole" genannt (Public Key Token), werden in die AssemblyRef-Tabelle eingetragen und sind in der Shell-Erweiterung für den GAC zu sehen. Übrigens steht in der AssemblyDef-Tabelle des Manifests der vollständige öffentliche Schlüssel, also nicht das Token.

Beim Eintrag des Assembly im GAC wird ein Unterverzeichnis angelegt, die Dateien des Assembly ins neue Verzeichnis kopiert und die GAC-Datenbank erhält einen neuen Eintrag. Dieser neue Datensatz verknüpft das neu angelegte Unterverzeichnis mit dem Namen des Assembly, seinem öffentlichen Schlüssel, einigen Versionsangaben und Attributen. Der GAC kann also nur Assemblies mit starken Namen aufnehmen. Private Assemblies haben nichts im GAC zu suchen. Assemblies mit starken Namen müssen aber nicht zwangsläufig im GAC installiert werden. Jede Anwendung kann in ihrem Verzeichnis Assemblies mit starken Namen unterbringen und die Anwendung für solch ein Assembly konfigurieren, statt ihr eventuell vorhandenes Gegenstück aus dem GAC benutzen zu müssen.
Der GAC kann übrigens mehrere Versionen eines logischen Assembly enthalten. So könnte es im GAC zum Beispiel die Versionen 1.0.0.0 und 2.0.0.0 der Calculus.dll geben. Wenn eine Anwendung mit der Version 1.0.0.0 entwickelt und getestet wurde, dann lädt die Laufzeitschicht für diese Anwendung die Version 1.0.0.0, selbst wenn es im GAC höhere Versionen von der Calculus.dll geben sollte.

Als Administrator kann man diese Vorgehensweise des Systems ändern. Empfehlenswert ist dies aber nicht - es sei denn, man kann sich einfach nicht von der "DLL-Hölle" trennen, die durch diese strenge Versionierung eigentlich beseitigt werden sollte. Solange Sie bei der strengen Versionierung bleiben, wird sich die Anwendung so verhalten, wie Sie es gewohnt sind, denn sie läuft mit demselben Code, mit dem sie auch entwickelt und getestet wurde. Übrigens bedeutet die Lösung mit dem GAC auch, dass die Version 2.0.0.0 der Calculus.dll in keiner Weise "abwärtskompatibel" zur Version 1.0.0.0 sein muss. (Auf den Aufbau der Versionsnummern werde ich im nächsten Abschnitt noch genauer eingehen.)

Die Signatur einer Datei mit einem privaten Schlüssel sichert die Identität der Datei. Wenn das Assembly in den GAC installiert wird, bildet das System eine Prüfsumme über die PE-Datei und vergleicht sie mit der Prüfsumme, die in der PE-Datei abgelegt wurde. Diese muss dazu vorher mit dem öffentlichen Schlüssel entschlüsselt werden, der in der Datei abgelegt wurde. Sind beide Werte identisch, so wurde der Dateiinhalt nicht verändert und Sie wissen, dass Sie einen öffentlichen Schlüssel benutzen, der zum privaten Schlüssel des Herausgebers passt. Das System erkennt bei der Installation nur, ob die Datei geändert wurde, in der das Manifest liegt. Erst beim späteren Laden des Assembly überprüft das System, ob eine der anderen Dateien des Assembly geändert wurde. (Auf die öffentlichen Schlüssel und Prüfsummen werde ich später noch zurückkommen.)

Bei diesem Mechanismus können Sie nicht sagen, wer der Herausgeber ist, bis Sie herausgefunden haben, welcher Herausgeber den öffentlichen Schlüssel produziert hat, den Sie benutzen. Außerdem wird vorausgesetzt, dass der Schlüssel des Herausgebers nicht verändert wurde. Sofern der Herausgeber des Assembly auch seine Identität bekannt geben möchte, muss er auf die Authenticode-Technik von Microsoft zurückgreifen, damit die Identitätsangabe zuverlässig ist.

Sobald eine Anwendung ein gemeinsames Assembly benutzen möchte, übergibt die Laufzeitschicht die erforderlichen Assembly-Attribute (Name, öffentlicher Schlüssel, Version und andere Attribute) an den GAC. Der GAC durchsucht seine Datenbank. Wird er fündig, gibt er das Unterverzeichnis an, in dem das gesuchte Assembly zu finden ist. Dadurch erhält der Aufrufer auch die Gewissheit, dass das zur Laufzeit geladene Assembly tatsächlich vom selben Herausgeber stammt wie das Assembly, die bei der Entwicklung des Programms benutzt wurde. Ist das gesuchte Assembly nicht verfügbar, so schlägt die Bindung fehl und das System meldet eine TypeLoadException-Ausnahme. (Auf die Bindung gehe im später noch genauer ein.)

Verzögerte Signatur

Die Schlüsselpaare werden mit Hilfe der Kryptographiefunktionen von Windows generiert. Die Schlüssel lassen sich in Dateien oder anderen Ablagen unterbringen. Große Firmen (wie Microsoft) können den ermittelten privaten Schlüssel in einer speziellen Hardware abspeichern, die im Safe verschlossen bleibt. Nur wenige Personen haben Zugang zu diesem privaten Schlüssel. Das verringert die Wahrscheinlichkeit dafür, dass der private Schlüssel in falsche Hände gelangt und seine Zuverlässigkeit verliert. Im Gegensatz dazu darf der öffentliche Schlüssel nach Bedarf weitergegeben werden.

Wenn Sie soweit sind, dass Ihr Assembly mit starkem Namen verpackt werden soll, müssen Sie den gesicherten privaten Schlüssel einsetzen und das Assembly quasi "unterschreiben". Aus naheliegenden Gründen kann es ungemein lästig werden, während der Entwicklung und dem Test des Assembly immer wieder die umständliche Zeremonie mit dem privaten Schlüssel aufführen zu müssen. Ein temporärer privater Schlüssel wäre in der Entwicklungsphase wesentlich praktischer. Sicherheitsaspekte würden bei diesem temporären Schlüssel nicht ganz so extrem im Vordergrund stehen wie beim "richtigen" privaten Schlüssel. Damit dies möglich ist, gibt es die verzögerte Signatur.

Die verzögerte Signatur ermöglicht die Entwicklung eines Assembly ohne privaten Schlüssel. Im wesentlichen speichern Sie den öffentlichen Schlüssel Ihrer Firma in einer Datei ab und übergeben den Namen der Datei an das Werkzeug, mit dem Sie das Assembly zusammenbauen. Wenn Sie zum Beispiel den AL.exe einsetzen, geben Sie den Dateinamen mit /key[file] an. Außerdem müssen Sie den AL.exe mit dem Schalter /delay[sign] darüber informieren, dass Sie ihm keinen privaten Schlüssel geben werden. Anschließend wird das gewählte Hilfsprogramm den öffentlichen Schlüssel in die AssemblyDef-Tabelle eintragen, damit auch andere Assemblies dieses Assembly benutzen können. Außerdem lässt das Programm in der resultierenden PE-Datei den entsprechenden Platz für die RSA-Signatur frei (aus dem öffentlichen Schlüssel kann das Programm den Platzbedarf ableiten). Bei dieser Gelegenheit wird keine Prüfsumme über die Datei gebildet.

Statt AL.exe mit den Schaltern /key[file] und /delay[sign] aufzurufen, können Sie die erforderlichen Angaben zur verzögerten Signatur auch direkt im Quelltext machen, und zwar mit den folgenden anwenderdefinierten Attributen:

  System.Reflection.AssemblyKeyFileAttribute 
  System.Reflection.AssemblyDelaySignAttribute

Der Konstruktor von AssemblyKeyFileAttribute erwartet den Namen der Datei, in welcher der öffentliche Schlüssel liegt, während der Konstruktor von AssemblyDelaySignAttribute mit einem Boolean zufrieden ist, aus dem hervorgeht, ob das Assembly verzögert signiert werden soll oder nicht.
An diesem Punkt hat das resultierende Assembly natürlich keine gültige Signatur. Normalerweise würde die Laufzeitschicht davon ausgehen, dass das Assembly manipuliert wurde, und es nicht laden. Damit die Laufzeitschicht das Assembly akzeptiert, müssen Sie die Laufzeitschicht anweisen, die Überprüfung dieses Assembly zu überspringen. Das geschieht mit dem Hilfsprogramm SN.exe aus dem .NET Framework SDK (mit dem Schalter -Vr).

Wenn Sie mit der Entwicklung und dem Test des Assembly fertig sind, müssen Sie das Ergebnis noch offiziell signieren, damit es verpackt und vertrieben werden kann. Für die digitale Unterschrift benutzen Sie wieder das Programm SN.exe, diesmal aber mit dem Schalter -R und dem Namen der Datei, in welcher der tatsächliche private Schlüssel zu finden ist. Das veranlasst SN.exe dazu, eine Prüfsumme über die Datei zu bilden, sie mit dem privaten Schlüssel zu unterschreiben und die digitale RSA-Signatur an dem Platz in der Datei unterzubringen, der speziell zu diesem Zweck reserviert wurde. Anschließend können Sie das vollständig unterschriebene Assembly in den Vertrieb geben.

Am Anfang dieses Abschnitts war davon die Rede, dass man das Schlüsselpaar auch auf einem bestimmten Hardware-Gerät unterbringen kann, zum Beispiel auf einer Smartcard. Damit die Schlüssel sicher sind, dürfen Sie nie in einer Datei abgespeichert werden. Spezielle Kryptographiedienste (CSPs) bieten nun Behälter an, die in diesem Sinne den Ort abstrahieren, an dem die Schlüssel untergebracht sind. Microsoft benutzt zum Beispiel einen CSP (Cryptographic Service Provider) mit einem Behälter, der den privaten Schlüssel bei Bedarf aus einer Smartcard ausliest. Wenn Sie Ihr Schlüsselpaar in einem CSP-Behälter untergebracht haben, setzen Sie aber nicht mehr den Schalter /keyf[ile] von AL.exe ein oder das anwenderdefinierte Attribute AssemblyKeyFileAttribute. Statt dessen verwenden Sie den Schalter /keyn[ame] von AL.exe und geben den Namen des Attributs System.Reflection.AssemblyKeyNameAttribute an.

Versionsnummern

Nehmen wir an, ich hätte eine Anwendung entwickelt, verpackt und vertrieben, die mehrere gemeinsame Assemblies enthält. Ein paar Monate später macht sich die Jeff Company an die Entwicklung einer neuen Version des Assembly JeffTypes. Diese neue Version beherrscht einige neue Tricks und hat einige Fehler nicht mehr, die sich bei den Kunden als ungemein "beliebt" erwiesen hatten. Nun könnte ich die Dateien des Assembly JeffTypes an meine Kunden schicken und sie auffordern, die neuen Dateien einfach auf die alten zu kopieren. Startet der Anwender anschließend wieder das Programm, erhält er den neuen Leistungsumfang und bleibt von den behobenen Fehlern verschont. Oder nicht?

Was geschieht, wenn die aktualisierten Assembliesdateien versehentlich einen neuen unangenehmen Fehler einführen? Dann arbeitet das Programm nicht mehr wie gewohnt und der Kunde regt sich zu recht auf. In den letzten Jahren war dieses Problem eigentlich noch wesentlich gravierender, denn viele Dateien wurden von mehreren Anwendungen gemeinsam benutzt. Und wenn eine dieser Anwendungen die gemeinsamen Dateien aktualisierte, mussten alle anderen Anwendungen mit den neuen Versionen arbeiten.

Es gab keinen einfachen Zusammenhang zwischen der Installation einer neuen Anwendung und dem plötzlichen seltsamen Verhalten einer anderen Anwendung, die nicht mehr richtig lief.

Heutzutage kann man nur dringendst empfehlen, keine Dateien mehr gemeinsam zu benutzen, sofern sich das irgendwie vermeiden lässt. Benutzen Sie private Assemblies, solange es geht. Platz auf der Festplatte ist so billig wie noch nie zuvor und die neuen Platten sind so riesig, dass man nicht viel gewinnt, wenn man ein paar Duplikate von der Platte löscht. Außerdem bedeutet es eine bessere Isolierung der Anwendungen voreinander, wenn man die Dateien getrennt hält. Dann hat die Aktualisierung der einen Anwendung keine Auswirkung mehr auf die anderen Programme. Natürlich steigt die Auslastung des Systemspeichers, wenn man mehrere ähnliche Assemblies gleichzeitig lädt. Im allgemeinen dürfte dem Benutzer aber eine funktionierende Anwendung wichtiger sein als ein kleinerer "Fußabdruck" der Programme im Speicher.

Letztlich läuft der ganze Aufwand auf eines hinaus: sobald eine Anwendung installiert ist und sauber läuft, soll es auch dabei bleiben. Das bedeutet im Prinzip, dass Sie die Assembly-Dateien einer Anwendung nie aktualisieren sollten. Natürlich werden gelegentlich Fehler gefunden und beseitigt, wodurch neue Versionen der Assemblies entstehen und sich das Versionsproblem erneut stellt. Wenn Sie das Beste aus beiden Welten möchten, muss die Laufzeitschicht also einen brauchbaren Mechanismus für die Versionsverwaltung anbieten.

Allerdings können Sie nur bei Assemblies mit starken Namen auf die Unterstützung der Laufzeitschicht zählen, wenn es um die Versionsverwaltung geht. Was die privaten Assemblies anbetrifft, setzt die Laufzeitschicht einfach die privaten Assemblies ein, die sie vorfindet, und zwar unabhängig von den Versionsinformationen.

Aufbau der Versionsnummern

Jedes Assembly hat eine bestimmte Versionsnummer. Diese Nummer setzt sich aus drei logischen Teilen zusammen, die insgesamt in Form von vier Zifferngruppen geschrieben werden. Im Bild B5 finden Sie als Beispiel die Versionsnummer 2.5.719.2. Die ersten beiden Zifferngruppen benennen die logische Version des Assembly. In diesem Beispiel arbeite ich also an der Version 2.5. Die dritte Zifferngruppe gibt die Build-Nummer des Assembly an, in diesem Fall 719. Wenn Ihre Firma das Assembly jeden Tag neu kompiliert, sollten Sie auch jeden Tag die Build-Nummer hochsetzen. Die letzte Zifferngruppe mit der einsamen 2 gibt die Revisionsnummer dieser Build-Version an. Muss Ihre Firma das Assembly aus irgendeinem Grund zweimal kompilieren, um zum Beispiel einen bestimmten Fehler zu beseitigen, der sonst den ganzen Betrieb aufhalten würde, sollten Sie auch die Revisionsnummer nachführen.

02-Admin05.gif

B5 Versionsangabe der Assemblies

Die Laufzeitschicht betrachtet Assemblies mit verschiedenen Versionsnummern als verschiedene Assemblies. Wenn eine Anwendung also die Version 2.0.0.0 eines bestimmten Assembly braucht, wird die Laufzeitschicht diese Anwendung immer an die Version 2.0.0.0 des Assembly binden. Wurden die Dateien der Version 2.0.0.0 gelöscht und die Version 2.5.0.0 des Assembly installiert, so kann die Laufzeitschicht die Anwendung nicht an das neue Assembly binden. Zumindest ist dies das übliche Verhalten der Laufzeitschicht bei solchen Versionsabweichungen. Ich werde später noch beschreiben, wie der Administrator dieses Verhalten ändern kann.

Findet die Laufzeitschicht aber zwei Assemblies mit derselben Kombination von Haupt- und Nebennummer, so bindet sie die Anwendung an die Version mit der höchsten Build- und Revisionsnummer. Anders gesagt, die Laufzeitschicht bindet die Anwendung auch dann an die Version 2.5.719.2, wenn die Anwendung bei ihrer Erstellung an die Version 2.5.100.7 gebunden wurde. Auch hierbei handelt es sich wieder um ein Standardverhalten der Laufzeitschicht, das vom Administrator geändert werden kann. (Die Bindungsregeln werden sich jedoch in der Beta 2 des .NET Frameworks noch einmal ändern.)

Die Versionsnummer können Sie entweder mit dem Assembly-Attribut System.Reflection.AssemblyVersionAttribute angeben oder mit dem Kommandozeilenschalter /version von AL.exe. Beide Methoden führen zur Aufnahme der angegebenen Versionsdaten ins Manifest des Assembly. Wenn Sie nicht explizit eine Versionsnummer vorgeben, werden die meisten Entwicklungswerkzeuge wohl die Standardnummer 0.0.0.0 eintragen.

Der Bequemlichkeit halber können die AssemblyVersionAttributes und AL.exe (mit dem /version-Schalter) Standardwerte für die Neben-, Build- und Revisionsnummer erzeugen. Die folgenden Zeilen sollen zeigen, wie man das ausnutzt:
[assembly:AssemblyVersion("1.*")] [assembly:AssemblyVersion("1.5.*")] [assembly:AssemblyVersion("1.5.2.*")]

Wenn CSC.exe oder AL.exe Versionsangaben mit Sternchen sehen, generieren sie automatisch Nummern für die fehlenden Teile. Das jeweilige Programm setzt die Nebennummer auf 0, die Build-Nummer auf die Anzahl der Tage, die seit dem ersten Januar 2000 verstrichen sind, und die Revisionsnummer auf die Zahl der Sekunden, die seit Mitternacht (Lokalzeit) verstrichen sind, geteilt durch zwei.

Dieser Algorithmus garantiert, dass Build- und Revisionsnummer immer unverwechselbar sind und stets steigen, falls der Entwickler sein Assembly im Laufe des Tages häufiger kompilieren sollte. Es gilt Lokalzeit, also nicht Greenwich Mean Time (GMT), denn die Build-Nummer soll sich ja nicht mitten am Tag ändern.

Beim Kompilieren eines Moduls müssen alle benutzten Assemblies angegeben werden. Bei der Zusammenstellung des AssemblyRef-Tabelle trägt der Compiler die vollständigen Versionsnummern der benutzten Assemblies ein. Sobald der MSIL-Code des Moduls einen importierten Typ benutzt, stellt die Laufzeitschicht fest, welches Assembly diesen Typ implementiert. Anhand des Namens und der Versionsangabe des Assembly findet die Laufzeitschicht heraus, welche Assembly-Datei zu laden ist.

Auch Assemblies haben eine Kultur

Zur Versionierung der Assemblies werden nicht nur Versionsnummern herangezogen, sondern auch Informationen über den Kulturkreis. So könnte ich zum Beispiel ein Assembly entwickeln, das ausschließlich für Deutschland vorgesehen ist, und ein weiteres mit schweizer Deutsch, eine mit U.S.-Englisch und so weiter. Der Begriff Kultur wird in diesem Zusammenhang zwar etwas seltsam verwendet, aber man sollte die Bezeichnung - wie so viele Bezeichnungen in der Computerwelt - wohl nicht auf die Goldwaage legen. Kulturen werden anhand eines Strings identifiziert, der eine primäre und eine sekundäre Angabe enthält (wie in RFC 1766 auf der IETF-Website unter http://www.ietf.org/rfc/rfc1766.txt beschrieben). Tabelle T2 zeigt einige Beispiele.

T2 Werte für den Kultur-String

Primärangabe

Sekundärangabe

Kultur

de

(keine)

Deutsch

de

At

Deutsch (Österreich)

de

Ch

Deutsch (Schweiz)

en

(keine)

Englisch

en

Gb

Englisch (Großbritannien)

en

Us

Englisch (USA)

Das bisschen Kultur können Sie dem Assembly mit dem Schalter /c[ulture]: von AL.exe beibringen oder mit dem Attribut System.Reflection.AssemblyCultureAttribute. Ein Beispiel:

// Lege "Deutsch (Schweiz)" als Kultur fest 
[assembly:AssemblyCulture("de-ch")]

Wenn Sie keinen expliziten Kultur-String zuweisen, wird das Assembly als kulturneutral angesehen.
Bei der Aufnahme eines Assembly in den GAC wird seine Kulturinformation in die Datenbank des GAC übernommen. Die Kulturangabe wird später bei der Bindung einer Anwendung an das Assembly berücksichtigt. Anders gesagt, es könnte im GAC zum Beispiel zwei verschiedene Calculus-Assemblies geben, die denselben Namen haben, dieselben Versionsnummern und auch denselben öffentlichen Schlüssel, aber verschiedene Kulturstrings.

Wenn die Laufzeitschicht nach dem passenden Assembly sucht, versucht sie zuerst, genau das Assembly zu finden, mit welchem die Anwendung entwickelt wurde. Deswegen enthält das AssemblyRef-Tabelle des Manifests auch Kulturangaben.

Es ist durchaus zu empfehlen, ein Assembly zu erstellen, das den MSIL-Code und die Standardressourcen der Anwendung enthält. Geben Sie bei der Erstellung dieses Assembly keine bestimmte Kultur an (lassen Sie also den Schalter /culture weg oder das anwenderdefinierte Attribut AssemblyCultureAttribute). Das ist sozusagen das Basis-Assembly, auf das andere Assemblies zur Erstellung und Manipulation von Typen zurückgreifen.

Sie können ein oder mehrere separate Assemblies erstellen, die nur Ressourcen enthalten - also überhaupt keinen MSIL-Code. Assemblies ohne MSIL-Code
werden auch "Satelliten" genannt. Geben Sie bei diesen SatellitenAssemblies genau die Kultur an, die den enthaltenen Ressourcen entspricht. Sie könnten zum Beispiel die Strategie verfolgen, für jeden Kulturkreis, den Sie beliefern möchten, einen separaten Satelliten zu bauen.

Friedliches nebeneinander

Zur vollständigen Unterstützung von gemeinsamen Assemblies ist Windows in der Lage, mehrere DLLs mit demselben Dateinamen in denselben Prozess zu laden. Bild B6 zeigt ein Beispiel, in dem ich ein Assembly namens App.exe benutze, das auf ein Assembly mit starkem Namen zurückgreift. Der Dateiname lautet Calculus.dll. Außerdem benutzt App.exe ein anderes, privates Assembly namens AdvMath.dll, das selbst wiederum ein Assembly mit starkem Namen benutzt. Der Dateiname dieses Assembly lautet - Sie ahnen es schon - wiederum Calculus.dll. Allerdings liegt dieses zweite Calculus-Assembly in einer anderen Version vor und hat zudem einen anderen öffentlichen Schlüssel (in diesem Beispiel ist die Kultur zwar dieselbe, aber das könnte auch anders sein).

02-Admin06.gif

B6 Verschiedene Assembly-Versionen, gleicher Dateiname

Die CLR ist in der Lage, mehrere Dateien selbst dann in denselben Adressraum zu laden, wenn sie dieselben Dateinamen tragen. Man könnte dies "Parallelbetrieb" (side-by-side execution) nennen. Diese ungewohnte Variante des Parallelbetriebs ist einer der wichtigsten Schlüssel zur Schließung der DLL-Hölle. Der beeindruckendste Aspekt dieses Parallelbetriebs dürfte wohl der Umstand sein, dass man neue Versionen eines Assembly entwickeln kann, ohne auf die vertrackte Abwärtskompatibilität achten zu müssen. Die Abwärtskompatibilität ignorieren zu dürfen erspart alle möglichen Klimmzüge in der Codierung und diverse Testläufe für die Anwendungen. Sie können Ihr Produkt schneller auf den Markt bringen.

Als Entwickler muss man diesen Parallelbetrieb berücksichtigen, damit sich keine mehr oder weniger subtilen Fehler ins Programm einschleichen. So könnte ein Assembly zum Beispiel eine Datei auf den Speicher abbilden und den Speicher benutzen, den dieses Dateiabbildungsobjekt bietet. Außerdem könnte eine andere Version des Assembly geladen werden, die ebenfalls versucht, ein Dateiabbildungsobjekt mit demselben Namen anzulegen. Dieses zweite Assembly erhält keinen neuen Speicher, sondern greift auf denselben Speicher zu, den schon das erste Assembly angefordert hat. Wird dies nicht sorgfältig codiert, so treten sich die beiden Versionen gegenseitig auf die Füße und das Verhalten des Programms ist nicht mehr vorhersagbar.

Auflösung von Typreferenzen

Nehmen wir folgende einfache Zeilen als Beispiel:

  public class App { 
   static public void Main(System.String[] args) { 
      System.Console.WriteLine("Hi"); 
   } 
}

Dieser Code wird kompiliert und in einem Assembly untergebracht. Nennen wir es App.exe. Zum Start dieses Programms muss die PE-Datei App.exe und die PE-Datei der CLR (MSCorEE.dll) in den neuen Prozess geladen werden. Sobald die Anwendung anläuft, wird die Laufzeitschicht initialisiert und richtet eine Anwendungsdomäne ein. Dann wird das Assembly App.exe in diese AppDomain geladen. Im wesentlichen begnügt sich die Laufzeitschicht erst einmal mit dem Manifest. Die Laufzeitschicht legt bei der Überprüfung des Manifests keinen allzu großen Eifer an den Tag - vorerst interessiert es sie zum Beispiel nicht, ob alle Dateien vorhanden und/oder die angesprochenen Assemblies verfügbar sind.

Dann wird der Eintrittspunkt MethodDefToken der Anwendung aus dem .NET-Kopf der PE-Datei ermittelt. MethodDefToken bezieht sich normalerweise auf eine Methode namens Main. Der Offset des MSIL-Codes dieser Methode in der Datei wird aus der MethodDef-Tabelle ausgelesen, just-in-time (JIT) in Maschinencode kompiliert (wobei der Code auch in Bezug auf die Typsicherheit überprüft wird) und ausgeführt. Der MSIL-Code für die obige Main-Methode lässt sich mit ILDasm leicht ermitteln:

.method /*06000001*/ public hidebysig static  
        void  Main(class System.String[] args) il managed 
{ 
  .entrypoint 
  // Codegröße         11 (0xb) 
  .maxstack  8 
  IL_0000:  ldstr      "Hi" 
  IL_0005:  call       void ['mscorlib'/* 23000001 */]System.Console 
                       /* 01000003 */::WriteLine(class System.String) 
  IL_000a:  ret 
} // Ende der Methode 'App::Main'

Sobald der Aufruf von System.Console.WriteLine ausgeführt werden soll, übernimmt die Laufzeitschicht die Kontrolle und versucht, die externe Referenz aufzulösen. Bei dieser Gelegenheit stellt die Laufzeitschicht fest, dass System.Console eine TypeRef-ID von 0x01000003 hat und dieser Typ in einer Assembly mit des AssemblyRef-ID 0x23000001 liegt. Wenn Sie die Metadaten genauer untersuchen, werden Sie feststellen, dass sich diese TypeRef- und AssemblyRef-Kennungen tatsächlich auf den Typ System.Console beziehen, der in des Assembly MSCorLib definiert wird.

Bei der Auflösung einer Typreferenz gibt es drei verschiedene Orte, an denen der Typ zu finden sein könnte:

  • Dieselbe Datei. Der Zugriff erfolgt auf einen Typ, der in derselben Datei liegt und früh gebunden wird. Der Typ wird direkt aus der Datei geladen und das Programm fährt mit seiner Arbeit fort.

  • Andere Datei, selbes Assembly. Die Laufzeitschicht überprüft, ob die angesprochene Datei tatsächlich in der FileRef-Tabelle des Manifests des aktuellen Assembly eingetragen ist und sucht dann in dem Verzeichnis, aus dem die Manifestdatei geladen wurde. Die Datei wird geladen und anhand der Prüfsumme überprüft. Die aufzurufende Funktion wird gesucht und das Programm fährt mit seiner Arbeit fort.

  • Andere Datei, anderes Assembly. Die Laufzeitschicht lädt die Datei des angesprochenen Assembly, in dem das Manifest zu finden ist. Sollte der gewünschte Typ nicht in dieser Datei liegen, wird die entsprechende Datei geladen. Die aufzurufende Funktion wird gesucht und das Programm fährt mit seiner Arbeit fort.

Die Metadatentabellen ModuleDef, ModuleRef und FileDef nennen Namen und Namensendung (Erweiterung) der Dateien. Die AssemblyRef-Tabelle benennt die Assemblies anhand der Dateinamen ohne Namensendung. Bei der Bindung an ein Assembly hängt das System die Endung .dll an, bevor es die Datei sucht.
Sollte bei der Auflösung einer Typreferenz ein Fehler auftreten (die Datei ist nicht zu finden, lässt sich nicht laden, die Prüfsumme stimmt nicht und so weiter), so meldet das System die Ausnahme System.TypeLoadException.

Im obigen Beispiel stellt die Laufzeitschicht fest, dass System.Console in einem anderen Assembly implementiert wird. Nun muss die Laufzeitschicht das Assembly suchen und die PE-Datei laden, in der das Manifest des Assembly liegt. Dann ermittelt sie anhand des Manifests, in welcher PE-Datei der Typ liegt. Liegt er ebenfalls in der Manifestdatei, ist die Suche beendet. Liegt er dagegen in einer anderen Datei, lädt die Laufzeitschicht die Datei und sucht in deren Metadaten nach den Methoden des Typs. Sobald die gesuchte Methode gefunden ist, wird sie JIT-kompiliert und ausgeführt. Bild B7 stellt die Typbindung als Flussdiagramm dar.

http://www.microsoft.com/02-Admin07.gif

B7 Typbindung

Wenn die Laufzeitschicht ein neues Assembly mit starkem Namen lädt, generiert sie aus dem eingebetteten öffentlichen Schlüssel ein öffentliches Schlüsselsymbol (public key token). Dieses wird mit dem öffentlichen Schlüsselsymbol aus dem AssemblyRef-Eintrag verglichen. Stimmen die Schlüssel nicht überein, wird eine TypeLoadException gemeldet. Auf diese Weise ist sichergestellt, dass das benutzte Assembly tatsächlich vom Besitzer des privaten Schlüssels stammt.

Wenn die Laufzeitschicht eine neue Datei lädt, bildet sie über diese neu geladene Datei eine Prüfsumme und vergleicht sie mit der Prüfsumme aus dem FileRef-Eintrag aus dem Manifest. Stimmen die Werte nicht überein, so wurde die Datei nachträglich verändert und ist somit nicht mehr vertrauenswürdig. Diese Überprüfung fällt bei jedem Laden einer Datei an und erfordert natürlich einen gewissen Rechenaufwand, der sich in der Laufgeschwindigkeit bemerkbar macht.

Fortgeschrittene Administrative Kontrolle (Konfiguration)

Im Abschnitt "Einfache administrative Kontrolle (Konfiguration)" habe ich in der letzten Folge kurz beschrieben, wie der Administrator die Art und Weise verändern kann, in der die Laufzeitschicht Assemblies sucht und einbindet. In dem Abschnitt habe ich beschrieben, wie der Administrator sämtliche Dateien eines Assembly in ein anderes Verzeichnis verlegen und eine passende XML-Konfigurationsdatei erstellen kann, mit deren Hilfe die Laufzeitschicht die Dateien findet.

In diesem Abschnitt möchte ich die Optionen genauer betrachten, die ein Administrator zur Konfiguration hat. Die Konfigurationswerte liegen alle in den folgenden XML-Dateien:

  • App.cfg (wobei App für den Namen der Anwendung steht). Diese Datei ist für die anwendungsspezifische Konfiguration zuständig und muss im selben Verzeichnis liegen, in dem auch die HauptAssemblies der Anwendung zu finden sind.

  • Admin.cfg. Diese Datei ist für die maschinenspezifische Konfiguration zuständig und muss im Windows-Verzeichnis liegen (zum Beispiel in C:\WinNT).

Mit Hilfe dieser Dateien hat der Administrator eine gewisse Kontrolle über die Bindungsregeln für Assemblies. Nehmen wir zum Beispiel an, App.exe benutze die Version 1.0.0.0 der Calculus.dll. Mit der App.cfg kann der Administrator dafür sorgen, dass alle Referenzen auf die Version 1.0.0.0 der Calculus.dll an die Version 2.0.0.0 der Calculus.dll gebunden werden.

Genauere Informationen darüber, wie die CLR die Assemblies bindet und wie die Informationen aus diesen XML-Konfigurationsdateien benutzt werden, finden Sie in der Dokumentation aus dem .NET Framework SDK unter "How the Runtime Locates Assemblies" (http://msdn.microsoft.com/ library/dotnet/cpguide/cpconhowruntimelocatesassemblies.htm).

Diese XML-Konfigurationsdateien erlauben es dem Herausgeber, sein Assembly zu aktualisieren, den Schwur abzulegen, dass sie abwärtskompatibel ist, und sie an den Administrator zu schicken. Der Administrator kann sich dafür entscheiden, dem Herausgeber zu glauben, das neue Assembly zu installieren und die Anwendung zu testen. Wichtig ist, dass das System auf diese Weise den Einsatz eines Assembly zulässt, das nicht genau zur Beschreibung in den Metadaten passt. Auch das steigert die Flexibilität.

Das genaue Format der XML-Konfigurationsdatei wird in der Dokumentation des .NET Framework SDKs beschrieben. Daher möchte ich an dieser Stelle nicht auf alle Details eingehen. Listing L2 zeigt ein Beispiel. Das AppDomain-Tag weist die Laufzeitschicht an, die privaten Assemblies zuerst im Anwendungsverzeichnis zu suchen und dann in den angegebenen Unterverzeichnissen des Anwendungsverzeichnisses, nämlich zuerst in bin und dann in AuxFiles. Auch hier möchte ich wieder auf den Abschnitt "How the Runtime Locates Assemblies" in der Dokumentation des .NET Framework SDKs verweisen. Er beschreibt die Verzeichnissuche ausführlicher, als ich es hier tun möchte.

L2 Die Konfigurationsdatei

<?xml version ="1.0"?> 
<Configurations> 
  <AppDomain PrivatePath="bin;Auxfiles"/> 
  <BindingMode> 
    <AppBindingMode Mode="normal"/> 
  </BindingMode> 
  <BindingPolicy> 
    <BindingRedir Name="Calculus.dll" 
                  Originator="8e47bf1a5ed0ec84" 
                  Version="*" VersionNew="3.3.10.0" 
                  UseLatestBuildRevision="no"/> 
  </BindingPolicy> 
  <Assemblies> 
    <CodeBaseHint Name="Calculus.dll" 
                  Originator="8e47bf1a5ed0ec84" 
                  Version="3.3.10.0" 
                  CodeBase="http://www.Wintellect.com/Calculus.dll"/> 
  </Assemblies> 
</Configurations>

Das BindingMode-Tag weist die Laufzeitschicht an, die üblichen Versionierungsregeln zu benutzen. Also sucht sie nach der höchsten Version eines Assembly, deren Haupt- und Nebennummer noch mit der Version übereinstimmt, mit welcher die Anwendung entwickelt wurde. Statt normal könnte man hier auch safe angeben. Dann würde die Laufzeitschicht genau die Assemblyversion suchen, mit welcher die Anwendung entwickelt wurde.

Das BindingRedir-Tag weist die Laufzeitschicht darauf hin, dass für alle Referenzen auf das gemeinsame Assembly Calculus.dll (mit dem Hersteller 8e47bf1a5ed0ec84) die Version 3.3.10.0 der Calculus.dll geladen werden soll. Das Attribut UseLatestBuildRevision legt fest, ob genau die angegebene Version benutzt werden muss oder die neuste Build.Revision dieses Assembly benutzt werden soll.

Das CodeBaseHint-Tag gibt der Laufzeitschicht einen Hinweis, wo sie suchen soll, falls die Assembly-Datei nicht auf der Maschine des Anwenders zu finden ist. In diesem Fall wird die Version 3.3.10.0 der Calculus.dll (mit dem Hersteller 8e47bf1a5ed0ec84) automatisch von http://www.Wintellect.com/Calculus.dll heruntergeladen.

Fazit

Assemblies (Assemblies) und Versionierung sollen einige komplizierte Probleme lösen, darunter folgende:

  • Wie kann man eine Anwendung so installieren, dass sie nie durch irgendwie eingeschleppte fehlerhafte aktuellere Dateiversionen unbrauchbar gemacht wird?

  • Sollte sich in einer installierten Anwendung ein Bug zeigen - wie kann man dann ein Assembly installieren, in welchem der Fehler behoben ist?

  • Was kann man tun, wenn die korrigierte Version eines Assembly einen neuen Fehler einschleppt?

  • Wie kann man einen Fehler in einem gemeinsamen Assembly korrigieren, ohne eine der anderen Anwendungen zu behindern? (Der Fehler zeigt sich vielleicht nur in einer Anwendung, wahrscheinlich aber in mehreren.)

Es ist wohl kaum möglich, eine Plattform zu entwickeln, in der alle diese Probleme gelöst sind, weil sich viele Lösungen gegenseitig widersprechen. In seiner kurzen Historie hat Windows bisher aber noch nie versucht, diese Probleme zu lösen. Derzeit schreiben die Entwickler neue Versionen ihrer DLLs, führen einige mehr oder weniger oberflächliche oder intensivere Tests durch und hoffen inständig, keine neuen Bugs eingebaut zu haben. Dann wird die neue DLL an einem gemeinsamen Ort installiert, an dem sie von mehreren Anwendungen benutzt wird. Und der Administrator hofft inständig, dass die betreffenden Anwendungen noch halbwegs vernünftig laufen und keine neuen seltsamen Verhaltensweisen zeigen. In der Praxis ist so manches Hoffen vergebens und der Anwender hat erst einmal ein Problem.

Außerdem kann eigentlich niemand so genau sagen, was eine "kompatible Komponente" ist. Ist die neue Version der Komponente zum Beispiel noch zur alten Version kompatibel, wenn sie eine neue Ausnahme melden kann? Meiner Ansicht nach nicht. Ich würde sogar soweit gehen, zu behaupten, dass jede Änderung bedeutet, dass die Versionen nicht mehr 100 Prozent kompatibel sind. Obwohl die Infrastruktur der Assemblies relativ komplex ist, stellt sie doch einen brauchbaren Mechanismus zur Lösung der oben genannten Probleme dar.

Anzeigen: