Vorhersage: Bewölkt

Entkopplung der Cloud mit MEF

Joseph Fultz
Chris Mabry

Beispielcode herunterladen.

Joseph FultzIn den letzten Monaten habe ich mit einem Kollegen an einem Projekt gearbeitet, bei dem das Microsoft Extensibility Framework (MEF) eine wichtige Rolle spielt. In diesem Artikel erläutern wir, wie Sie die Cloudbereitstellung mit MEF flexibler und besser verwaltbar machen können. MEF – und ähnliche Frameworks wie z. B. Unity – sind Softwarestrukturen, die Entwicklern Verwaltungsaufgaben wie Abhängigkeitsauflösungen, Objekterstellung und Instanziierung abnehmen. Ab und zu müssen vielleicht noch Factorymethoden geschrieben oder abhängige Objekte innerhalb eines Konstruktors oder einer erforderlichen Initialisierungsmethode erstellt werden, aber in den meisten Fällen sind diese Schritte dank Frameworks wie MEF nicht mehr nötig.

Durch die Verwendung von MEF in Kombination mit der StorageClient-API in unserer Bereitstellung können neue Klassen bereitgestellt und verfügbar gemacht werden, ohne dass die Webrollen wiederverwendet oder erneut bereitgestellt werden müssen. Außerdem können aktualisierte Typversionen ohne vollständige erneute Bereitstellung in der Cloud implementiert werden, indem einfach die Anwendung wiederverwendet wird. Wir nutzen hier zwar MEF, mit Unity, Castle Windsor, StructureMap oder anderen ähnlichen Containern sollten aber die gleichen Ergebnisse erzielt werden können, wobei die Unterschiede vor allem in der Syntax und der Semantik für die Typregistrierung liegen.

Design und Bereitstellung

Hier gilt: Von nichts kommt nichts. In unserem Beispiel sind bestimmte Konstruktionsstandards und einige zusätzliche Schritte für die Bereitstellung erforderlich. Zunächst: Wenn Sie daran gewöhnt sind, eine Abhängigkeitsinjektion (Dependency Injection, DI) oder einen Kompositionscontainer einzusetzen, möchten Sie wahrscheinlich auch hier bei der Trennung von Implementierung und Schnittstelle in Ihrem Code bleiben. Kein Problem: Alle unsere Implementierungen konkreter Klassen weisen nur Vererbungen auf, die sich zu einem Schnittstellentyp zurückverfolgen lassen. Das bedeutet nicht, dass jede Klasse direkt von einer Schnittstelle erbt. Die Klassen weisen aber in der Regel Abstraktionsschichten auf, die Mustern wie „Schnittstelle – Virtuell – Konkret“ folgen.

In Abbildung 1 ist zu sehen, dass die primäre Klasse, die uns interessiert, eine solche Kette aufweist und dass eine ihrer erforderlichen Eigenschaften abstrahiert ist. Die Abstraktion vereinfacht das Austauschen von Bestandteilen oder Hinzufügen von zusätzlichen Funktionen in Form einer neuen Bibliothek, die den gewünschten Vertrag exportiert (in diesem Fall die Schnittstelle). Neben der vereinfachten Zusammenstellung ist ein weiterer Vorteil der Abstraktion des Klassendesigns, dass das Testen über simulierte Schnittstellen besser funktioniert.

Class Diagram
Abbildung 1: Klassendiagramm

Die etwas schwerer zu erfüllende Anforderung ist die Änderung des Bereitstellungsmodells für die Anwendung. Da wir unseren Katalog mit Imports und Exports zur Laufzeit erstellen und ohne erneute Bereitstellung aktualisieren möchten, müssen wir die binären Dateien bereitstellen, die die konkreten Klassen außerhalb der Webrollenbereitstellung enthalten. Dies erfordert schon beim Anwendungsstart einen zusätzlichen Schritt. In Abbildung 2 wird dieser Schritt mit der „Global.asax“ umgesetzt: Sie ruft die von uns erstellte MEFContext-Hilfsklasse auf.

Building the Catalog at Startup
Abbildung 2: Erstellen des Katalogs beim Anwendungsstart

Laufzeitzusammensetzung

Da wir den Katalog aus Dateien im Speicher laden, müssen diese Dateien zunächst in den Cloudspeichercontainer übertragen werden. Das Übertragen der Dateien in den Windows Azure-Speicherort ist daher als Teil des Bereitstellungsprozesses einzuplanen. Am einfachsten lässt sich diese Aufgabe mit Windows Azure-PowerShell-Cmdlets (wappowershell.codeplex.com) und einigen Postbuildschritten durchführen. In unserem Beispiel verschieben wir die Binärdateien jedoch manuell mit dem Windows Azure Storage Explorer (azurestorageexplorer.codeplex.com).

Wir haben ein Projekt mit einer allgemeinen Diagnostics-Klasse, einer Customer-Entität und einer Reihe von Regelbibliotheken erstellt. Alle Regelbibliotheken müssen von einer Schnittstelle vom Typ IBusinessRule<t> erben und diese exportieren, wobei "t" für die Entität steht, mit deren Hilfe die Regeln erzwungen werden. Die Import-Bestandteile der Klassendeklaration für eine Regel sind folgende:

[Export(typeof(IBusinessRule<ICustomer>))]
public class CustomerNameRule : IBusinessRule<ICustomer>
{
  [Import(typeof(IDiagnostics))]
  IDiagnostics _diagnostics;
    ...
}

Sie sehen hier den Export und die Diagnostics-Abhängigkeit, die von MEF injiziert wird, wenn nach dem Regelobjekt gefragt wird. Es ist wichtig zu wissen, was exportiert wird, da dies wiederum den Vertrag ergibt, zu dessen Bedingungen die gewünschten Instanzen aufgelöst werden. Mit Microsoft .NET Framework 4.5 werden verschiedene Verbesserungen in MEF eingeführt, durch die einige der aktuellen Beschränkungen für generische Typen im Container wegfallen. Derzeit können Sie z. B. Typen wie IBusinessRule<ICustomer> registrieren und abrufen, aber keine Typen wie IBusiness-Rule<t>. In einigen Fällen benötigen Sie aber alle Instanzen eines Typs über den tatsächlichen Vorlagentyp hinaus. Zurzeit lässt sich dies am einfachsten durchführen, indem Sie einen Vertragsnamen registrieren, der einer bereits etablierten Konvention in Ihrem Projekt oder Ihrer Lösung entspricht. Für unser Beispiel ist die oben gezeigte Deklaration geeignet.

Wir haben zwei Regeln (eine für die Telefonnummer und eine für den Namen) und eine Diagnostics-Bibliothek, die jeweils über den MEF-Container zur Verfügung stehen werden. Als Erstes müssen wir uns die Bibliotheken aus dem Windows Azure-Speicher vornehmen und sie in eine lokale Ressource (lokales Verzeichnis) übertragen, sodass wir sie in einen DirectoryCatalog laden können. Hierzu verwenden wir Funktionsaufrufe in der Application_Start-Methode von Global.asax:

// Store the local directory for later use (directory catalog)
MEFContext.CacheFolderPath = 
  RoleEnvironment.GetLocalResource("ResourceCache").RootPath.ToLower();
MEFContext.InitializeContainer();

Wir nehmen uns nur den erforderlichen Ressourcenpfad vor, der als Teil der Webrolle konfiguriert ist, und rufen dann die Methode zum Einrichten des Containers auf. Diese Initialisierungsmethode ruft wiederum UpdateFromStorage auf, um die Dateien abzurufen, und BuildContainer, um den Katalog und dann den MEF-Container zu erstellen.

Die UpdateFromStorage-Methode durchläuft einen vordefinierten Container, wodurch die Dateien in diesem Container in den lokalen Ressourcenordner heruntergeladen werden. Der erste Teil dieser Methode wird in Abbildung 3 dargestellt.

Abbildung 3: Erster Teil von UpdateFromStorage

// Could also pull from config, etc.
string containerName = CONTAINER_NAME;
// Using development storage account
CloudStorageAccount storageAccount = 
  CloudStorageAccount.DevelopmentStorageAccount;
// Create the blob client and use it to create the container object
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
// Note that here is where the container name is passed
// in order to get to the files we want
CloudBlobContainer blobContainer = new CloudBlobContainer(
  storageAccount.BlobEndpoint.ToString() + 
  "/" + containerName,
  blobClient);
// Create the options needed to get the blob list
BlobRequestOptions options = new BlobRequestOptions();
options.AccessCondition = AccessCondition.None;
options.BlobListingDetails = BlobListingDetails.All;
options.UseFlatBlobListing = true;
options.Timeout = new TimeSpan(0, 1, 0);

Im ersten Teil haben wir den Speicherclient eingerichtet, um die benötigten Daten zu erhalten. Im Rahmen dieses Szenarios werden dabei alle Dateien abgerufen. Diese Vorgehensweise bietet sich in Fällen an, bei denen Dateien aus einem Speicher in eine lokale Ressource abgerufen werden sollen. Wenn Sie Dateien jedoch gezielter abrufen möchten, können Sie der options.AccessCondition-Eigenschaft eine IfMatch-Bedingung hinzufügen. Dafür müssen für die Blobs beim Hochladen ETags festgelegt werden. Außerdem können Sie den Aktualisierungsaspekt bei der Neuerstellung des MEF-Containers optimieren, indem Sie den Zeitpunkt der letzten Aktualisierung speichern und die AccessCondition "IfModifiedSince" anwenden.

In Abbildung 4 wird der zweite Teil von UpdateFromStorage gezeigt.

Abbildung 4: Zweiter Teil von UpdateFromStorage

// Iterate over the collect
// Grab the files and save them locally
foreach (IListBlobItem item in blobs)
{
  string fileAbsPath = item.Uri.AbsolutePath.ToLower();
  // Just want the file name ...
  fileAbsPath = 
    fileAbsPath.Substring(fileAbsPath.LastIndexOf('/') + 1);
  try
  {
    Microsoft.WindowsAzure.StorageClient.CloudPageBlob pageblob =
      new CloudPageBlob(item.Uri.ToString());
    pageblob.DownloadToFile(MEFContext.CacheFolderPath + fileAbsPath, 
      options);
  }
  catch (Exception)
  {
    // Ignore exceptions, if we can't write it's because
    // we've already got the file, move on
    }
}

Nachdem der Speicherclient fertig eingerichtet wurde, durchlaufen wir einfach die Blobelemente und laden sie in die Ressource herunter. Je nach Bedingungen und Zielsetzungen des Downloads können Sie bei dieser Operation die Ordnerstrukturen lokal replizieren oder eine Ordnerstruktur entsprechend den Konventionen erstellen. Manchmal ist eine Ordnerstruktur erforderlich, um Namenskonflikte zu vermeiden. Wir nehmen hier die ganz einfache Methode und laden alle Dateien in einen Speicherort herunter, da wir wissen, dass es in diesem Szenario nur zwei oder drei DLLs gibt.

Auf diese Weise haben wir die Dateien alle an einem Ort vorliegen und müssen nur noch den Container erstellen. In MEF wird der Kompositionscontainer aus einem oder mehreren Katalogen erstellt. In diesem Fall verwenden wir einen DirectoryCatalog, da er den Verweis auf das Verzeichnis und das Herunterladen der vorhandenen Binärdateien vereinfacht. Der Code zum Registrieren der Typen und Vorbereiten des Containers ist daher kurz und einfach:

// Store the container for later use (resolve type instances)
var catalog = new DirectoryCatalog(CacheFolderPath);
MEFContainer = new CompositionContainer(catalog);
MEFContainer.ComposeParts();

Jetzt führen wir die Site aus und sehen eine Liste von im Container vorhandenen Typen, wie in Abbildung 5 gezeigt.

Initial Exports
Abbildung 5: Erste Exporte

Dies ist jedoch nicht das gesamte Containerabbild, sondern nur das Ergebnis der Abfrage für die IDiagnostics-Schnittstelle und aller Exporte des Typs IBusinessRule<ICustomer>. Wie die Abbildung zeigt, ist vor dem Hochladen einer neuen Geschäftsregelbibliothek in den Speichercontainer dafür jeweils ein Typ vorhanden.

Wir haben die NewRules.dll in den Speicherort gelegt und müssen sie nun in die Anwendung laden. Idealerweise sollte die Neuerstellung des Containers mittels Überwachung der Dateiliste im Speichercontainer ausgelöst werden. Dies kann wieder ganz einfach mit einer Abfrage unter Verwendung der AccessCondition "IfModifiedSince" erreicht werden. Wir haben uns jedoch hier für eine manuelle Vorgehensweise entschieden und klicken in unserer Testanwendung auf "Update Catalog". In Abbildung 8 werden die Ergebnisse angezeigt.

Updated Rules Exports
Abbildung 8: Aktualisierte Regelexporte

Wir wiederholen diese Schritte einfach, um den Katalog zu erstellen und den Container zu initialisieren, und haben dann eine neue zu erzwingende Regelbibliothek. Beachten Sie, dass wir die Anwendung nicht neu gestartet oder erneut bereitgestellt haben, sondern lediglich neuen Code in der Umgebung ausführen. Das einzige Problem ist hier, dass eine Synchronisierungsmethode benötigt wird, da der Code sonst erfolglos versucht, den Kompositionscontainer zu verwenden, während der Verweis ausgetauscht wird:

var catalog = new DirectoryCatalog(CacheFolderPath);
CompositionContainer newContainer = 
  new CompositionContainer(catalog);
newContainer.ComposeParts();
lock(MEFContainer)
{
  MEFContainer = newContainer;
}

Der Hauptgrund für die Erstellung eines sekundären Containers und das anschließende Austauschen des Verweises besteht in der möglichst kurzen Sperrung des Containers und seiner möglichst kontinuierlichen Verfügbarkeit.

Zur weiteren Verfeinerung der Codebasis könnte nun ein eigener benutzerdefinierter Katalogtyp implementiert werden, z. B. AzureStorageCatalog, wie in Abbildung 9 gezeigt. Leider weist das aktuelle Objektmodell weder eine richtige Schnittstelle noch eine leicht wiederverwendbare Basis auf, sodass wir uns hier mit Vererbung und Kapselung zufrieden geben müssen. Durch das Implementieren einer Klasse, die der AzureStorageCatalog-Auflistung ähnelt, kann ein einfaches Modell der Instanziierung des benutzerdefinierten Katalogs erzielt werden, das direkt im Kompositionscontainer verwendet werden kann.

Abbildung 9: AzureStorageCatalog

public class AzureStorageCatalog:ComposablePartCatalog
{
  private string _localCatalogDirectory = default(string);
  private DirectoryCatalog _directoryCatalog = 
    default(DirectoryCatalog);
  AzureStorageCatalog(string StorageSetting, string ContainerName)
    :base()
  {
    // Pull the files to the local directory
    _localCatalogDirectory = 
      GetStorageCatalog(StorageSetting, ContainerName);
    // Load the exports using an encapsulated DirectoryCatalog
    _directoryCatalog = new DirectoryCatalog(_localCatalogDirectory);
  }
  // Return encapsulated parts
  public override IQueryable<ComposablePartDefinition> Parts
  {
    get { return _directoryCatalog.Parts; }
  }
  private string GetStorageCatalog(string StorageSetting, 
    string ContainerName)
  {  }
}

Aktualisieren bestehender Funktionen

Das Hinzufügen neuer Funktionen zu unserer Bereitstellung ist recht einfach, das Gleiche gilt aber leider nicht für die Aktualisierung bestehender Funktionen oder Bibliotheken. Die Vorgehensweise ist zwar besser als eine vollständige erneute Bereitstellung, sie ist aber immer noch recht aufwendig, da die Dateien in den Speicher übertragen und die lokalen Ressourcenordner von den relevanten Webrollen aktualisiert werden müssen. Wir verwenden aber auch die Rollen wieder, da wir die AppDomain entladen und wieder laden müssen, um die im Container gespeicherte Typdefinition zu aktualisieren. Auch wenn Sie Kompositionscontainer und Typen in eine sekundäre AppDomain laden und den Ladevorgang von hier aus versuchen zu starten, wird der angeforderte Typ von der AppDomain, in der Sie den Typ anfordern, dennoch aus den zuvor geladenen Metadaten geladen. Die einzige Lösung ist das Senden der Entitäten an die sekundäre AppDomain und das Hinzufügen von benutzerdefiniertem Marshalling im Gegensatz zur Verwendung der exportierten Typen in der primären AppDomain. Diese Vorgehensweise erschien uns jedoch problematisch; bereits die doppelte AppDomain an sich schien ein Problem darzustellen. Eine einfachere Lösung ist daher die Wiederverwendung der Rollen, sobald die neuen Binärdateien zur Verfügung stehen.

Eine gute Nachricht gibt es zu Windows Azure-Upgradedomänen. In meinem Artikel "Windows Azure-Bereitstellungsdomänen" (msdn.microsoft.com/magazine/hh781019) von Februar 2012 beschreibe ich, wie die Upgradedomänen einzeln durchgegangen und Instanzen in ihnen neu gestartet werden. Das Positive daran ist, dass der Betrieb der Site nicht unterbrochen werden muss und keine vollständige erneute Bereitstellung nötig ist. Während der Aktualisierung treten jedoch eventuell zwei unterschiedliche Verhaltensweisen auf. Dies ist aber ein annehmbares Risiko, da das Gleiche auch für ein paralleles Update bei einer vollständigen Bereitstellung gilt.

Sie können den Prozess zwar so konfigurieren, dass er innerhalb der Bereitstellung durchgeführt wird, dabei entsteht jedoch ein Koordinationsproblem: Die Neustarts der Instanzen müssten koordiniert werden, sodass entweder eine führende Instanz oder eine Abstimmung der Instanzen untereinander erforderlich wäre. Bevor wir aber nun künstliche Intelligenz in die Webrollen implementieren, erschien es uns einfacher, einen Überwachungsprozess einzusetzen und dazu die zuvor beschriebenen Windows Azure-Cmdlets zu verwenden.

Es gibt neben dem kleinen Funktionsbereich, den wir hier beleuchtet haben, noch viele weitere Gründe für die Verwendung eines Frameworks wie MEF. In diesem Artikel wollten wir zeigen, wie Sie mit der Kombination aus in Windows Azure integrierten Funktionen und einem Framework vom Typ Komposition/DI/Inversion of Control eine dynamische Cloudanwendung erstellen können, die sich problemlos an die berühmt-berüchtigten Änderungen in letzter Minute anpassen lässt.

Joseph Fultz ist Softwarearchitekt bei Hewlett-Packard Co. und Mitglied der HP.com Global IT-Gruppe. Zuvor war er Softwarearchitekt bei Microsoft und arbeitete gemeinsam mit dessen wichtigsten Unternehmens- und ISV-Kunden an der Definition von Architekturen und dem Entwurf von Lösungen.

Chris Mabry ist leitender Entwickler bei Hewlett-Packard Co. Sein Fokus besteht zurzeit in der Leitung eines Teams zur Entwicklung einer vielseitigen UI, die auf dienstfähigen Clientframeworks basiert.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Chris Brooks