Innovationen

Expando-Objekte in C# 4.0

Dino Esposito

Beispielcode herunterladen.

Obwohl .NET die dynamische Typisierung mittels Reflektion ermöglicht, wird Code für Microsoft .NET Framework meist basierend auf statischer Typisierung geschrieben. JScript setzt bereits seit 10 Jahren ein dynamisches Typsystem auf .NET auf, ebenso wie Visual Basic. Die Bezeichnung der statischen Typisierung bedeutet, dass jeder Ausdruck ein bekannter Typ ist. Typen und Zuweisungen werden zur Kompilierzeit validiert, dabei werden die meisten Typisierungsfehler im Voraus abgefangen.

Eine bekannte Ausnahme besteht in dem Versuch, zur Laufzeit eine Umwandlung durchzuführen. Ist dabei der Quelltyp nicht mit dem Zieltyp kompatibel, kann dies zu einem dynamischen Typisierungsfehler führen.

Die statische Typisierung ist für Leistung und Klarheit optimal geeignet, setzt aber voraus, dass Sie sowohl Code als auch zugehörige Daten bereits im Voraus detailliert kennen. Heutzutage besteht die Notwendigkeit, diese Einschränkung ein bisschen zu lockern. Wenn es darum geht, über die statische Typisierung hinauszugehen, stehen in der Regel drei verschiedene Optionen zur Verfügung: dynamische Typisierung, dynamische Objekte sowie indirekte oder reflektionsbasierte Programmierung.

Im Rahmen der .NET-Programmierung ist die Reflektion bereits seit .NET Framework 1.0 verfügbar und wird überwiegend in speziellen Frameworks, z. B. IoC (Inversion of Control)-Container, eingesetzt. Bei dieser Art Framework werden Typabhängigkeiten zur Laufzeit aufgelöst, wodurch von Ihnen geschriebener Code mit einer Schnittstelle zusammenarbeiten kann, ohne dass der konkrete Typ hinter dem Objekt sowie dessen tatsächliches Verhalten bekannt sein müssen. Mit der Reflektion von .NET können Sie bestimmte indirekte Programmierungen implementieren, bei denen von Ihnen erstellter Code mit einem Zwischenobjekt kommuniziert, das wiederum Aufrufe an eine feste Schnittstelle weiterleitet. Sie übergeben den aufzurufenden Membernamen als Zeichenfolge und sichern sich so die Flexibilität, diesen auch über eine externe Quelle lesen zu können. Die Schnittstelle des Zielobjekts ist fest und unveränderlich, es steht also immer eine bekannte Schnittstelle hinter jedem Aufruf, den Sie anhand von Reflektion tätigen. 

Bei der dynamischen Typisierung ignoriert von Ihnen kompilierter Code die statische Struktur der Typen, die zur Laufzeit erkannt werden. Damit werden auch sämtliche Typprüfungen erst zur Laufzeit ausgeführt. Die Schnittstelle, die Ihr Code anspricht, ist immer noch fest und unveränderlich, jedoch kann der von Ihnen verwendete Wert zu verschiedenen Zeitpunkten unterschiedliche Schnittstellen zurückgeben.

In .NET Framework 4 werden einige neue Features eingeführt, mit denen Sie über statische Typen hinausgehen können. Eine ausführliche Abhandlung zum neuen dynamischen Schlüsselwort finden Sie in Ausgabe Mai 2010. In diesem Artikel konzentriere ich mich auf die Unterstützung dynamisch definierter Typen wie z. B. Expando-Objekte und dynamische Objekte. Bei dynamischen Objekten können Sie die Schnittstelle des Typs programmgesteuert definieren, anstatt sie aus einer Definition einzulesen, die statisch in einigen Assemblys hinterlegt ist. In dynamischen Objekten vereinigen sich die formale Klarheit statisch typisierter Objekte und die Flexibilität dynamischer Typen.

Szenarios für dynamische Objekte

Dynamische Objekte ersetzen jedoch nicht die wertvollen Eigenschaften von statischen Typen. Statische Typen bilden die Basis der Softwareentwicklung, und in absehbarer Zukunft wird sich daran auch nichts ändern. Mit der statischen Typisierung lassen sich Typisierungsfehler zuverlässig zur Kompilierzeit erkennen, und deswegen müssen für erstellten Code keine Prüfungen zur Laufzeit mehr ausgeführt werden, was eine schnellere Verarbeitung möglich macht. Zudem sorgt die Notwendigkeit dieses Kompilierschritts dafür, dass sich sowohl Entwickler als auch Architekten sorgfältig um den Entwurf der Software und die Definition öffentlicher Schnittstellen für interagierende Schichten kümmern.

Natürlich gibt es Situationen, in denen relativ gut strukturierte Datenblöcke programmgesteuert verarbeitet werden. Im Idealfall würden diese Daten durch Objekte verfügbar gemacht. Aber stattdessen erhalten Sie einen einfachen Datenstrom, und zwar unabhängig davon, ob Sie die Daten per Netzwerkverbindung erhalten oder aus einer Datenträgerdatei einlesen. Um mit diesen Daten zu arbeiten, stehen Ihnen zwei Möglichkeiten zur Verfügung: Sie können einen indirekten Ansatz oder einen Ad-hoc-Typ verwenden.

Im ersten Fall nutzen Sie eine generische API, die als Proxy fungiert und Abfragen sowie Updates für Sie ausführt. Im zweiten Fall arbeiten Sie mit einem spezifischen Typ, der die Daten, mit denen Sie arbeiten, perfekt abbildet. Die Frage ist, wer einen solchen Ad-hoc-Typ erstellen soll.

In einigen Segmenten von .NET Framework sind bereits gute Beispiele dafür vorhanden, wie von internen Modulen Ad-hoc-Typen für spezifische Datenblöcke generiert werden. Zu diesen Beispielen zählt eindeutig auch ASP.NET Web Forms. Wenn Sie eine ASPX-Ressource anfordern, wird vom Webserver der Inhalt der ASPX-Serverdatei abgerufen. Dieser Inhalt wird dann in eine Zeichenfolge hochgeladen und im Rahmen der Verarbeitung in eine HTML-Antwort umgewandelt. Somit erhalten Sie einen relativ gut strukturierten Text, mit dem Sie nun arbeiten können.

Um diese Daten bearbeiten zu können, müssen Sie die auf Serversteuerelemente verweisenden Referenzen kennen, diese ordnungsgemäß instanziieren und auf einer Seite verknüpfen. Dies erreichen Sie, indem Sie für jede Anforderung einen XML-basierten Parser verwenden. Allerdings müssen Sie bei jeder Anforderung die zusätzlichen Parserkosten zahlen, die möglicherweise inakzeptabel hoch sind.

Aufgrund dieser zusätzlichen Kosten hat das ASP.NET-Team entschieden, einen einmaligen Schritt einzuführen, in dem die Analyse des Markup in eine dynamisch kompilierbare Klasse erfolgt. Im Ergebnis wird eine einfache Markupsequenz wie diese über eine Ad-hoc-Klasse verarbeitet, die aus der CodeBehind-Klasse der Web Forms-Seite abgeleitet ist:

<html>
<head runat="server">
  <title></title>
</head>
<body>
  <form id="Form1" runat="server">
    <asp:TextBox runat="server" ID="TextBox1" /> 
    <asp:Button ID="Button1" runat="server" Text="Click" />
    <hr />
    <asp:Label runat="server" ID="Label1"></asp:Label>
  </form>
</body>
</html>

Abbildung 1 zeigt die Laufzeitstruktur der aus dem Markup generierten Klasse an. Die grau unterlegten Methodennamen beziehen sich auf interne Prozeduren, mit denen die Analyse der Elemente unter Verwendung von runat=server in Instanzen der Serversteuerelemente erfolgt.

image: The Structure of a Dynamically Created Web Forms Class

Abbildung 1 Struktur einer dynamisch erstellten Web Forms-Klasse

Sie können diese Methode in jeder Situation anwenden, in der Ihre Anwendung wiederholt externe Daten verarbeitet. Stellen Sie sich vor, ein XML-Datenstrom wird von der Anwendung abgerufen. Für die Verarbeitung von XML-Daten stehen mehrere APIs zur Verfügung, von XML DOM bis LINQ-to-XML. Entweder starten Sie eine indirekte Verarbeitung, indem Sie die XML DOM-API oder die LINQ-to-XML-API abfragen, oder Sie nutzen dieselben APIs, um eine Analyse der Rohdaten in Ad-hoc-Objekte vorzunehmen.

In .NET Framework 4 wird über die dynamischen Objekte eine alternative und einfachere API geboten, mit der auf Basis der Rohdaten dynamische Typen erstellt werden können. Folgende XML-Zeichenfolge gibt ein kurzes Beispiel:

<Persons>
  <Person>  
    <FirstName> Dino </FirstName>
    <LastName> Esposito </LastName>
  </Person>
  <Person>
    <FirstName> John </FirstName>
    <LastName> Smith </LastName>
  </Person>  
</Persons>

Um das in einen programmierbaren Typ umzuwandeln, nutzen Sie in .NET Framework 3.5 wahrscheinlich folgenden Code in Abbildung 2.

Abbildung 2 Datenupload in ein Person-Objekt mit LINQ-to-XML

var persons = GetPersonsFromXml(file);
foreach(var p in persons)
  Console.WriteLine(p.GetFullName());

// Load XML data and copy into a list object
var doc = XDocument.Load(@"..\..\sample.xml");
public static IList<Person> GetPersonsFromXml(String file) {
  var persons = new List<Person>();

  var doc = XDocument.Load(file);
  var nodes = from node in doc.Root.Descendants("Person")
              select node;

  foreach (var n in nodes) {
    var person = new Person();
    foreach (var child in n.Descendants()) {
      if (child.Name == "FirstName")
        person.FirstName = child.Value.Trim();
      else
        if (child.Name == "LastName")
          person.LastName = child.Value.Trim();
    }
    persons.Add(person);
  }

  return persons;
}

Vom Code werden Raw-Inhalte mit LINQ-to-XML in eine Instanz der Person-Klasse geladen:

public class Person {
  public String FirstName { get; set; }
  public String LastName { get; set; }
  public String GetFullName() {
    return String.Format("{0}, {1}", LastName, FirstName);
  }
}

In .NET Framework 4 wird eine andere API für denselben Zweck geboten. Diese API ist auf die neue ExpandoObject-Klasse ausgerichtet und für direktes Schreiben geeignet. Es ist nun nicht mehr nötig, eine Person-Klasse zu planen, zu schreiben, zu debuggen, zu testen und zu implementieren. Im Folgenden erfahren Sie mehr über ExpandoObject.

Verwenden der ExpandoObject-Klasse

Expando-Objekte wurden nicht für .NET Framework entwickelt, es gab sie schon mehrere Jahre vor der .NET-Einführung. Ich hörte zum ersten Mal Mitte der 1990er-Jahre davon, als der Begriff zur Beschreibung von JScript-Objekten verwendet wurde. Bei einem Expando-Objekt handelt es sich um ein befüllbares Objekt, dessen Struktur gänzlich zur Laufzeit definiert wird. In .NET Framework 4 wird es wie ein klassisches verwaltetes Objekt gehandhabt – außer dass die Struktur nicht aus einer Assembly gelesen, sondern vollständig dynamisch erstellt wird.

Ein Expando-Objekt ist perfekt geeignet, um sich ändernde Informationen, wie z. B. den Inhalt einer Konfigurationsdatei, dynamisch abzubilden. Im Folgenden wird erklärt, wie die ExpandoObject-Klasse genutzt wird, um den Inhalt des bereits erwähnten XML-Dokuments zu speichern. Der vollständige Quellcode ist in Abbildung 3 dargestellt.

Abbildung 3 Datenupload in ein Expando-Objekt mit LINQ-to-XML

public static IList<dynamic> GetExpandoFromXml(String file) { 
  var persons = new List<dynamic>();

  var doc = XDocument.Load(file);
  var nodes = from node in doc.Root.Descendants("Person")
              select node;
  foreach (var n in nodes) {
    dynamic person = new ExpandoObject();
    foreach (var child in n.Descendants()) {
      var p = person as IDictionary<String, object>);
      p[child.Name] = child.Value.Trim();
    }

    persons.Add(person);
  }

  return persons;
}

Von der Funktion wird eine Liste mit dynamisch definierten Objekten zurückgegeben. Mithilfe von LINQ-to-XML können Sie die Markupknoten analysieren und für jeden eine ExpandoObject-Instanz erstellen. Die Namen der einzelnen Knoten unter <Person> werden im Expando-Objekt zu einer neuen Eigenschaft. Der Wert der Eigenschaft setzt sich aus dem Text im Knoten zusammen. Basierend auf dem XML-Inhalt erhalten Sie schließlich ein Expando-Objekt, dessen FirstName-Eigenschaft auf „Dino“ festgelegt ist.

In Abbildung 3 können Sie sehen, dass eine Indexer-Syntax zur Füllung des Expando-Objekts verwendet wird. Das werde ich nun etwas näher erläutern.

Einblick in die ExpandoObject-Klasse

Die ExpandoObject-Klasse gehört zum System.Dynamic-Namespace und wird in der System.Core-Assembly definiert. ExpandoObject stellt ein Objekt dar, dessen Member zur Laufzeit dynamisch hinzugefügt oder entfernt werden können. Die Klasse ist versiegelt und implementiert zahlreiche Schnittstellen:

public sealed class ExpandoObject : 
  IDynamicMetaObjectProvider, 
  IDictionary<string, object>, 
  ICollection<KeyValuePair<string, object>>, 
  IEnumerable<KeyValuePair<string, object>>, 
  IEnumerable, 
  INotifyPropertyChanged;

Wie Sie sehen können, wird der Inhalt der Klasse anhand von verschiedenen aufzählbaren Schnittstellen, darunter IDictionary<String, Object> und IEnumerable ausgedrückt. Zudem ist auch IDynamicMetaObjectProvider implementiert. Dabei handelt es sich um die Standardschnittstelle, die eine gemeinsame Nutzung eines Objekts in der Dynamic Language Runtime (DLR) durch Programme ermöglicht, die nach den Vorgaben des DLR-Interoperabilitätsmodells geschrieben sind. Mit anderen Worten: Nur Objekte, welche die IDynamicMetaObjectProvider-Schnittstelle implementieren, können in dynamischen .NET-Sprachen gemeinsam genutzt werden. So kann ein Expando-Objekt zum Beispiel an eine IronRuby-Komponente übergeben werden. Mit einem herkömmlichen verwalteten .NET-Objekt ist das allerdings nicht ganz einfach, denn Sie erzielen nicht das dynamische Verhalten.

Die ExpandoObject-Klasse implementiert auch die INotifyPropertyChanged-Schnittstelle. Diese ermöglicht es, dass von der Klasse ein PropertyChanged-Ereignis ausgelöst wird, sobald ein Member hinzugefügt oder geändert wird. Die Unterstützung der INotifyPropertyChanged-Schnittstelle ist unerlässlich für die Verwendung von Expando-Objekten in den Front-Ends von Silverlight- und Windows Presentation Foundation-Anwendungen.

Sie erstellen eine ExpandoObject-Instanz ebenso wie ein anderes .NET-Objekt, nur die Variable zum Speichern der Instanz ist dynamisch:

dynamic expando = new ExpandoObject();

Um zu diesem Zeitpunkt eine Eigenschaft zum Expando-Objekt hinzuzufügen, weisen Sie einfach einen neuen Wert zu, wie nachfolgend gezeigt:

expando.FirstName = "Dino";

Es ist unerheblich, dass über den FirstName-Member keine Informationen vorliegen und weder Typ noch Sichtbarkeit spezifiziert sind. Schließlich handelt es sich um dynamischen Code, und aus diesem Grund macht es einen Unterschied, ob Sie das var-Schlüsselwort zum Zuweisen einer ExpandoObject-Instanz zu einer Variable verwenden:

var expando = new ExpandoObject();

Der Codeabschnitt wird kompiliert und funktioniert gut. Bei dieser Definition können Sie jedoch keiner FirstName-Eigenschaft einen Wert zuweisen. Die in System.Core definierte ExpandoObject-Klasse verfügt über kein Member dieser Art. Genauer gesagt gibt es bei der ExpandoObject-Klasse keine öffentlichen Member.

Das ist ein wichtiger Punkt. Wenn der statische Typ eines Expando-Objekts dynamisch ist, gelten die ausgeführten Operationen zwingend als dynamisch, darunter auch die Membersuche. Lautet der statische Typ aber ExpandoObject, gelten die Operationen als regulär und die Membersuche wird zur Kompilierzeit ausgeführt. Der Compiler erkennt also, dass der dynamische Typ ein besonderer Typ ist, jedoch erkennt er ExpandoObject nicht als besonderen Typ.

In Abbildung 4 sehen Sie die IntelliSense-Optionen von Visual Studio 2010 für den Fall, dass ein Expando-Objekt als dynamischer Typ deklariert bzw. als einfaches .NET-Objekt behandelt wird. Im zweiten Fall werden von IntelliSense die System.Object-Standardmember sowie eine Liste der Erweiterungsmethoden für Auflistungsklassen angezeigt.

image: Visual Studio 2010 IntelliSense and Expando Objects

Abbildung 4 Visual Studio 2010 IntelliSense und Expando-Objekte

Beachten Sie, dass einige handelsübliche Tools unter bestimmten Umständen ein anderes als das Basisverhalten bieten können. Abbildung 5 zeigt das Tool ReSharper 5.0, mit dem die derzeit für ein Objekt definierten Member aufgelistet werden. Sind die Member programmgesteuert über einen Indexer hinzugefügt worden, ist dies nicht möglich.

Figure 5 The ReSharper 5.0 IntelliSense at Work with Expando Objects

Abbildung 5 ReSharper 5.0 IntelliSense im Zusammenspiel mit Expando-Objekten

Um einem Expando-Objekt eine Methode hinzuzufügen, definieren Sie diese einfach als Eigenschaft. Davon ausgenommen sind die Delegaten Action<T> und Func<T>, die für den Verhaltensausdruck verwendet werden. Hier ein Beispiel:

person.GetFullName = (Func<String>)(() => { 
  return String.Format("{0}, {1}", 
    person.LastName, person.FirstName); 
});

Von der Methode GetFullName wird eine Zeichenfolge zurückgegeben, die aus einer Kombination der Eigenschaften für Vorname und Nachname entstanden ist. Dabei wird davon ausgegangen, dass diese beiden Eigenschaften für das Expando-Objekt verfügbar sind. Wenn Sie versuchen, bei Expando-Objekten auf nicht vorhandene Member zuzugreifen, gibt das System die Ausnahme RuntimeBinderException aus. 

XML-basierte Programme

Um die bisher vorgestellten Konzepte zu verbinden, zeige ich Ihnen nun ein Beispiel, in dem die Struktur von Daten und Benutzeroberfläche in einer XML-Datei definiert ist. Der Dateiinhalt wird in einer Auflistung von Expando-Objekten analysiert und von der Anwendung verarbeitet. Jedoch sind für diese Anwendung nur dynamisch präsentierte Informationen zulässig, und es ist kein bestimmter statischer Typ zwingend vorgeschrieben.

Der Codeabschnitt in Abbildung 3 umfasst eine Liste dynamisch definierter Expando-Objekte des Person-Typs. Wenn Sie nun dem XML-Schema einen neuen Knoten hinzufügen, wird erwartungsgemäß im Expando-Objekt eine neue Eigenschaft generiert. Falls der Membername aus einer externen Quelle gelesen werden muss, sollten Sie die Indexer-API verwenden, um den Namen zum Expando-Objekt hinzuzufügen. Die ExpandoObject-Klasse implementiert explizit die Schnittstelle IDictionary<String, Object>. Um folglich die Indexer-API oder die Add-Methode verwenden zu können, muss die ExpandoObject-Schnittstelle vom Wörterbuchtyp separiert werden:

(person as IDictionary<String, Object>)[child.Name] = child.Value;

Aufgrund dieses Verhaltens ist nur eine Bearbeitung der XML-Datei erforderlich, um einen anderen Datensatz verfügbar zu machen. Aber wie können diese dynamischen, sich ändernden Daten verarbeitet werden? Ihre Benutzeroberfläche muss flexibel genug sein, um variable Datensätze zu verarbeiten.

Im nächsten einfachen Beispiel zeigen Sie nur Daten über die Konsole an. Angenommen, die XML-Datei enthält einen Abschnitt, der die erwartete Benutzeroberfläche beschreibt – was auch immer das in Ihrem Kontext heißt. Hier also mein Beispiel:

<Settings>
    <Output Format="{0}, {1}" 
      Params="LastName,FirstName" /> 
  </Settings>

Diese Daten werden mithilfe von folgendem Code in ein weiteres Expando-Objekt geladen:

dynamic settings = new ExpandoObject();
  settings.Format = 
    node.Attribute("Format").Value;
  settings.Parameters = 
    node.Attribute("Params").Value;

Die Hauptprozedur besitzt folgende Struktur:

public static void Run(String file) {
    dynamic settings = GetExpandoSettings(file);
    dynamic persons = GetExpandoFromXml(file);
    foreach (var p in persons) {
      var memberNames = 
        (settings.Parameters as String).
        Split(',');
      var realValues = 
        GetValuesFromExpandoObject(p, 
        memberNames);
      Console.WriteLine(settings.Format, 
        realValues);
    }
  }

Das Expando-Objekt beinhaltet das Format der Ausgabe sowie die Membernamen, deren Werte angezeigt werden sollen. Arbeiten Sie mit dem dynamischen Person-Objekt, müssen die Werte für die spezifizierten Member geladen werden. Dazu können Sie zum Beispiel folgenden Code verwenden:

public static Object[] GetValuesFromExpandoObject(
  IDictionary<String, Object> person, 
  String[] memberNames) {

  var realValues = new List<Object>();
  foreach (var m in memberNames)
    realValues.Add(person[m]);
  return realValues.ToArray();
}

Da ein Expando-Objekt immer IDictionary<String, Object> implementiert, können Sie mit der Indexer-API Werte abrufen und festlegen.

Zum Abschluss werden die vom Expando-Objekt abgerufenen Werte in einer Liste an die Konsole übergeben, in der sie dann angezeigt werden. In Abbildung 6 werden zwei Bildschirme für die im Beispiel verwendete Konsolenanwendung angezeigt, bei der nur die Struktur der zugrunde liegenden XML-Datei unterschiedlich ist.

Image: Two Sample Console Applications Driven by an XML File

Abbildung 6 Zwei Beispiele für Konsolenanwendungen auf Basis einer XML-Datei

Es handelt sich zugegebenermaßen um ein sehr einfaches Beispiel, aber die für ein Funktionieren erforderlichen Mechanismen gelten auch für interessantere Beispiele. Probieren Sie es aus, und schicken Sie uns Ihr Feedback!

Dino Esposito ist der Autor des bei Microsoft Press erschienenen Titels „Programming ASP.NET MVC“ und Mitverfasser von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2008). Esposito lebt in Italien und ist ein weltweit gefragter Referent bei Branchenveranstaltungen. Sie finden seinen Blog unter weblogs.asp.net/despos.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Eric Lippert